Update: 評論區有同窗提出經過域名獲取 IP 地址時可能遭遇攻擊,感謝提醒。本人非安全專業相關人士,瞭解很少,實在慚愧。前端
說到 Web 安全,咱們前端可能接觸較多的是 XSS 和 CSRF。工做緣由,在所負責的內部服務中遭遇了SSRF 的困擾,在此記錄一下學習過程及解決方案。SSRF(Server-Side Request Forgery),即服務端請求僞造,是一種由攻擊者構造造成由服務端發起請求的一個安全漏洞。通常狀況下,SSRF 攻擊的目標是從外網沒法訪問的內部系統。web
SSRF 造成的緣由大都是因爲服務端提供了從其餘服務器應用獲取數據的功能且沒有對目標地址作過濾與限制。好比從指定 URL 地址獲取網頁文本內容,加載指定地址的圖片,下載等等。攻擊者可根據程序流程,使用應用所在服務器發出攻擊者想發出的 http 請求,利用該漏洞來探測生產網中的服務,能夠將攻擊者直接代理進內網中,可讓攻擊者繞過網絡訪問控制,能夠下載未受權的文件,能夠直接訪問內網,甚至可以獲取服務器憑證。sql
筆者負責的內部 web 應用中有一個下載文件的接口 /download
,其接受一個 url 參數,指向須要下載的文件地址,應用向該地址發起請求,下載文件至應用所在服務器,而後做後續處理。問題便來了,應用所在服務器在這裏成了跳板機,攻擊者利用這個接口至關於取得了內網權限,可以進行很多具備危害的操做。json
SSRF 帶來的危害有:安全
通用的解決方案有:服務器
因爲筆者的應用 /download
接口請求的文件地址比較固定,所以採用了白名單 IP 的方式。固然,筆者也學習了一下更加全面的解決方案,下面給出安所有門同事的思路:網絡
協議限制(默認容許協議爲 HTTP、HTTPS)、30x跳轉(默認不容許 30x 跳轉)、統一錯誤信息(默認不統一,統一錯誤信息避免惡意攻擊經過錯誤信息判斷)app
IP地址判斷:dom
contents-type
是否爲 application/json
解決 URL 獲取器和 URL 解析器不一致的方法爲:解析 URL 後去除 RFC3986 中 user、pass 並從新組合 URLasync
而後是按照以上思路實現的 Node.js 版本的處理 SSRF 漏洞的主要函數的代碼:
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 漏洞的原則,結合上述幾個函數,就能大體完成。
最後,一句話總結:永遠不要相信用戶的輸入!
本文首發於個人博客(點此查看),歡迎關注