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 漏洞的原则,结合上述几个函数,就能大体完成。
最后,一句话总结:永远不要相信用户的输入!
本文首发于个人博客(点此查看),欢迎关注