記一次Nodejs安全工單的處理過程_20171226

zhiqiang21發表於2019-02-16

事件原因:

之前使用Nodejs開發的一個網站。在網站上有一個頁面有個功能,允許使用者上傳圖片或者貼上一張圖片連結。服務端讀取使用者上傳的圖片資訊或者是請求使用者填寫的圖片連結獲取圖片資訊。

如果是使用者使用上傳功能,前端可以在input控制元件上做下限制上傳檔案的型別,後端再做下校驗,以保障獲取圖片檔案的合法(或者是安全)。

如果是使用者填寫的圖片連結,其中隱藏的安全隱患就比較大了。如果使用者填寫不是正常的httphttps或者是ftp類的連結,而是curl這種可以在服務端執行的命令,服務端拿到使用者的連結不作處理直接請求的話,可能會對公司造成不可估量的額損失(比如:內網伺服器資訊洩露、更嚴重是內網伺服器被攻擊)。

事件的處理方法:

在公司是有專門的安全組來做Web安全這塊兒工作的。這個事件發生的時候,我首先是簡訊收到了伺服器的報警,線上出現了大量的FATAL日誌。這個時候立刻是登上伺服器排查,緊接著就被公司的安全組某位同事在內部聊天工具上通知,網站收到了ssrf(伺服器端請求偽造)攻擊。第一時間對這個介面進行了下線處理,然後評估了安全的解決方案,再次上線該介面。

那麼在服務端具體的防範ssrf攻擊的方法時什麼呢,請接著往下看。

服務端的解決方案

ssrf具體原理就是在服務端偽造請求,請求伺服器的資源。上面的案例場景我也提到了。我有個功能是會請求一張外網圖片連結獲取圖片資訊的。而使用者填寫的這個圖片連結很可能就是違法的請求。當我服務端拿到這個連結後,並沒有按照預期去請求外網資源,而是請求了內網服務的資源。這樣我的伺服器就被攻擊。

那麼什麼原因可能造成ssrf漏洞呢:

  1. web服務存在請求外網資源的功能
  2. 請求的url引數是外接使用者可以控制的
  3. 服務端在請求外網資源前,沒有做域名和IP校驗(是否請求的是內網資源)
  4. 僅僅通過正則匹配來判斷IP段(域名指向的也可能是內網IP,如果IP是十進位制表示法沒有問題,但IP還有十六進位制和十進位制、二進位制表示法)

由上面提到的原因其實已經可以知道一些解決方法:

1.校驗使用者填寫連結的協議頭

在拿到使用者填寫的連結時做下校驗。看是否是合法的httphttpsftp協議頭。當然這個校驗為了減輕伺服器壓力應該在前端和後端都校驗。如不過不是合法的請求協議頭直接拒絕和返回請求資源失敗的錯誤訊息。(虛擬碼如下)

const allowReqprotocol = [
        `http`, 
        `https`,
        `ftp`,
    ];

const reqProtocol = request.protocol;

// 強制校驗使用者輸入圖片連結的協議頭是否為自己網站允許的協議頭
if (allowReqprotocol.indexOf(reqProtocol) < 0) {
    yog.log.warning(imgUrl);
    res.json(errorMsg.imgTypeError());
    return;
}

2.檢驗使用者請求的IP指向的是否為內網ip

上面已經介紹過僅僅通過正則沒辦法完全校驗是否是合法的IP段。其實還有一種方法就是把我們的IP段轉化為int來進行判斷。剛好npm上有個包ip-to-int可以滿足我們的需求。我們可以把兩者結合起來一起來判斷請求連結的IP是否為內網IP。

const dns = require(`dns`);
const url = require(`url`);
const ipToInt = require(`ip-to-int`);

const parseUrl = url.parse(rqUrlString);
const hostname = parseUrl.hostname; 

// 使用Nodejs的dns模組根據域名解析出域名對應的ip地址
dns.lookup(hostname, (err, address, family) => {

    //加入我的內網IP段為 180.xxx.xxx.xxx  180.255.255.255
    const ipRegx = /^180./;
    const ipArr = [ipToInt(180.0.0.0).toInt(), ipToInt(180.255.255.255).toInt()];

    
    if(ipRegx.test(address) || ipRang(address)){
        // 拒絕請求
    } else {
        // 繼續請求
    }
});


// 判斷一個ip是否在一個陣列之間
function ipRang(ip, array) {
    
    // ip在給定的ip段之間
    return true;
    // ip不在給定的ip段之間
    return false;
}

3.最後的檢驗

經過上面的檢驗已經基本上可以保證如果有請求外網的Url ,請求的在服務端執行以後指向的也是外網資源。那麼當資源到達服務端以後,為了保障資源的安全性。我們對請求的內容做最後的校驗。比如我僅僅是想請求圖片資源,而且是限制了幾種格式。比如:png、jpg、jpeg。

    const allowImgContentType = [
        `image/jpeg`,
        `image/png`,
        `image/gif`
    ];
    
    
   if( allowImgContentType.indexOf(response.headers[`content-type`]) < 0){
   
    // 返回的內容為非法資源
   }

4.假如允許請求內網資源

上面的方法基本上都是如果請求的是內網資源都是直接拒絕掉了。但是假如有請求內網的需求怎麼辦呢。如果內網的資源對外部開發,肯定也是特定的機器。這些機器也肯定是經過op做特殊的隔離處理的、沒有敏感的公司資訊資源。那麼當我們的請求到達公司內網後怎麼保證該請求始終請求的是特定的IP呢?

其實我們只需要將請求的連結的host和在header投中和請求機器的ip做下繫結就好。

// 通過dns模組解析出host對應的ip  這裡是address
// 並且通過url模組解析出port path 和querystring 重新拼接請求的url

const bindURLString = `${parseUrl.protocol}//${address}:${reqUrlPort}${pathName}?${queryString}`;

// 然後在請求發出去的時候設定下header頭,下面是我使用request模組發出請求時設定引數

const options = {
   `url`: bindURLString,
   `encoding`: `binary`,
   `rejectUnauthorized`: false,        
   `headers`: {
       `Host`: bindDnsObj.hostname    //這裡繫結請求的host
   }
};

這樣做就是強制請求去請求特定的機器。而不會請求其它機器。

相關文章