Web 安全漏洞 SSRF 簡介及解決方案

Update: 掘金評論區有同窗提出經過域名獲取 IP 地址時可能遭遇攻擊,感謝提醒。本人非安全專業相關人士,瞭解很少,實在慚愧。前端


說到 Web 安全,咱們前端可能接觸較多的是 XSS 和 CSRF。工做緣由,在所負責的內部服務中遭遇了SSRF 的困擾,在此記錄一下學習過程及解決方案。SSRF(Server-Side Request Forgery),即服務端請求僞造,是一種由攻擊者構造造成由服務端發起請求的一個安全漏洞。通常狀況下,SSRF 攻擊的目標是從外網沒法訪問的內部系統。web

SSRF 造成的緣由大都是因爲服務端提供了從其餘服務器應用獲取數據的功能且沒有對目標地址作過濾與限制。好比從指定 URL 地址獲取網頁文本內容,加載指定地址的圖片,下載等等。攻擊者可根據程序流程,使用應用所在服務器發出攻擊者想發出的 http 請求,利用該漏洞來探測生產網中的服務,能夠將攻擊者直接代理進內網中,可讓攻擊者繞過網絡訪問控制,能夠下載未受權的文件,能夠直接訪問內網,甚至可以獲取服務器憑證。sql

筆者負責的內部 web 應用中有一個下載文件的接口 /download,其接受一個 url 參數,指向須要下載的文件地址,應用向該地址發起請求,下載文件至應用所在服務器,而後做後續處理。問題便來了,應用所在服務器在這裏成了跳板機,攻擊者利用這個接口至關於取得了內網權限,可以進行很多具備危害的操做。json

SSRF 帶來的危害有:安全

  • 能夠對外網、服務器所在內網、本地進行端口掃描,獲取一些服務的 banner 信息;
  • 攻擊運行在內網或本地的應用程序(好比溢出);
  • 對內網 web 應用進行指紋識別,經過訪問默認文件實現;
  • 攻擊內外網的 web 應用,主要是使用 get 參數就能夠實現的攻擊(好比 struts2,sqli 等);
  • 利用 file 協議讀取本地文件等。

通用的解決方案有:服務器

  1. 過濾返回信息。驗證遠程服務器對請求的響應是比較容易的方法。若是 web 應用是去獲取某一種類型的文件,那麼在把返回結果展現給用戶以前先驗證返回的信息是否符合標準;
  2. 統一錯誤信息,避免用戶能夠根據錯誤信息來判斷遠端服務器的端口狀態;
  3. 限制請求的端口爲 http 經常使用的端口,好比 80, 443, 8080, 8090;
  4. 白名單內網 ip。避免應用被用來獲取獲取內網數據,攻擊內網;
  5. 禁用不須要的協議。僅僅容許 http 和 https 請求。能夠防止相似於file:///,gopher://,ftp:// 等引發的問題。

因爲筆者的應用 /download 接口請求的文件地址比較固定,所以採用了白名單 IP 的方式。固然,筆者也學習了一下更加全面的解決方案,下面給出安所有門同事的思路:網絡

  1. 協議限制(默認容許協議爲 HTTP、HTTPS)、30x跳轉(默認不容許 30x 跳轉)、統一錯誤信息(默認不統一,統一錯誤信息避免惡意攻擊經過錯誤信息判斷)
  2. IP地址判斷:app

    • 禁止訪問 0.0.0.0/8,169.254.0.0/16,127.0.0.0/8 和 240.0.0.0/4 等保留網段
    • 若 IP 爲 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 私有網段,請求該 IP 地址並判斷響應 contents-type 是否爲 application/json
  3. 解決 URL 獲取器和 URL 解析器不一致的方法爲:解析 URL 後去除 RFC3986 中 user、pass 並從新組合 URL

而後是按照以上思路實現的 Node.js 版本的處理 SSRF 漏洞的主要函數的代碼:dom

const dns = require('dns')
const parse = require('url-parse')
const ip = require('ip')
const isReservedIp = require('martian-cidr').default

const protocolAndDomainRE = /^(?:https?:)?\/\/(\S+)$/

const localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/
const nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/

/**
 * 檢查連接是否合法
 * 僅支持 http/https 協議
 * @param {string} string
 * @returns {boolean}
 */
function isValidLink (string) {
    if (typeof string !== 'string') {
        return false
    }

    var match = string.match(protocolAndDomainRE)
    if (!match) {
        return false
    }

    var everythingAfterProtocol = match[1]
    if (!everythingAfterProtocol) {
        return false
    }

    if (localhostDomainRE.test(everythingAfterProtocol) ||
        nonLocalhostDomainRE.test(everythingAfterProtocol)) {
        return true
    }

    return false
}

/**
 * @param {string} uri
 * @return string
 * host 解析爲 ip 地址
 * 處理 SSRF 繞過:URL 解析器和 URL 獲取器之間的不一致性
 *
 */
async function filterIp(uri) {
    try {
        if (isValidLink(uri)) {
            const renwerurl = renewUrl(uri)
            const parseurl = parse(renwerurl)
            const host = await getHostByName(parseurl.host)
            const validataResult = isValidataIp(host)

            if(!validataResult) {
                return false
            } else {
                return renwerurl
            }
        } else {
            return false
        }
    } catch (e) {
        console.log(e)
    }
}

/**
 * 根據域名獲取 IP 地址
 * @param {string} domain
 */
function getHostByName (domain) {
    return new Promise((resolve, reject) => {
        dns.lookup(domain, (err, address, family) => {
            if(err) {
                reject(err)
            }
            resolve(address)
        })
    })
}

/**
 * @param {string} host
 * @return {array} 包含 host、狀態碼
 *
 * 驗證 host ip 是否合法
 * 返回值 array(host, value)
 * 禁止訪問 0.0.0.0/8,169.254.0.0/16,127.0.0.0/8,240.0.0.0/4 保留網段
 * 若訪問 10.0.0.0/8,172.16.0.0/12,192,168.0.0/16 私有網段,標記爲 PrivIp 並返回
 */

function isValidataIp (host) {
    if ((ip.isV4Format(host) || ip.isV6Format(host)) && !isReservedIp(host)) {
        if (ip.isPrivate(host)) {
            return [host, 'PrivIp']
        } else {
            return [host, 'WebIp']
        }
    } else {
        return false
    }
}

/**
 * @param {string} uri
 * @return {string} validateuri
 * 解析並從新組合 url,其中禁止'user' 'pass'組合
 */

function renewUrl(uri) {
    const uriObj = parse(uri)
    let validateuri = `${uriObj.protocol}//${uriObj.host}`
    if (uriObj.port) {
        validateuri += `:${uriObj.port}`
    }
    if (uriObj.pathname) {
        validateuri += `${uriObj.pathname}`
    }
    if (uriObj.query) {
        validateuri += `?${uriObj.query}`
    }
    if (uriObj.hash) {
        validateuri += `#${uriObj.hash}`
    }
    return validateuri
}

對於最主要的可能出現漏洞的接口處理函數,因爲各邏輯不一樣,這裏就不給出具體實現。可是隻要按照上面提出的規避 SSRF 漏洞的原則,結合上述幾個函數,就能大體完成。async

最後,一句話總結:永遠不要相信用戶的輸入!

本文首發於個人博客(點此查看),歡迎關注

相關文章
相關標籤/搜索