Web 安全
XSS 跨站脚本攻击
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
攻击者可以通过 XSS 攻击实现以下操作:
- 获取页面的数据,如 DOM、cookie、localStorage
- DOS 攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器
- 破坏页面结构
- 流量劫持(将链接指向某网站)
XSS:
-
盲目信任用户提交的内容
-
string -> DOM
- document.write
- element.innerHTML
- SSR(user_data)
XSS 的特点:
- 通常难以从 UI 上感知(暗地执行脚本)
- 窃取用户信息(cookie / token)
- 绘制 UI(例如弹窗),诱骗用户点击,填写表单
反射型 XSS
不涉及数据库,仅在 URL 上攻击
-
攻击者构造出特殊的 URL,其中包含恶意代码
-
用户打开带有恶意代码的 URL,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器
-
用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
-
恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
举例:
// 某个 query 参数为
`host/path/?param=<script>alert('xss')</script>`
// 服务端
public async render(ctx) {
const { param } = ctx.query;
ctx.status = 200;
ctx.body = `<div>${param}</div>`
}
存储型 XSS
最严重、最厉害的(对所有用户可见)
- 攻击者将恶意代码注入到数据库里
- 用户访问目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
常见于带有用户保存数据的网站功能,如论坛发帖、评论区、用户私信等。
举例:
// 服务端:没有过滤
public async submit(ctx) {
const { content, id } = ctx.request;
// 没有对 content 过滤
await db.save({
content,
id
});
}
public async render(ctx) {
const { content } = await db.query({
id: ctx.query.id
})
// 可能 content 直接执行脚本
ctx.body = `<div>${content}</div>`
}
// 浏览器端:用户恶意提交
fetch("/submit", {
body: JSON.stringify({
id: "1",
content: `<script>alert("xss")</script>`
})
});
DOM 型 XSS
不需要服务器参与,恶意攻击的发起和执行都在浏览器完成
- 攻击者构造出特殊的 URL,其中包含恶意代码
- 用户打开带有恶意代码的 URL
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 型 XSS 跟前两种 XSS 的区别:
- DOM 型:取出和执行的代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞
- 而另外两种:属于服务端的安全漏洞
举例:
// 某个 query 参数为
`host/path/?param=<script>alert('xss')</script>`
// 浏览器端
const content = new URL(location.href).searchParams.get("param");
const divEle = document.createElement("div");
// 恶意脚本被注入
divEle.innerHTML = content;
document.body.append(divEle);
Mutation-based XSS
- 利用了浏览器渲染 DOM 的特性(独特优化)
- 不同浏览器,会有区别(按浏览器进行攻击)
举例:
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
<!-- 实际的渲染 -->
<div>
<noscript><p title=" </noscript>
<img src="x" onerror="alert(1)">
"">"
</div>
由于 img 的 src 错误,触发了 onerror。
预防 XSS 攻击
永远不信任用户的提交内容,不要将用户提交内容直接转换成 DOM
现成的防御工具:
-
前端
- 主流框架默认防御 XSS
- google-closure-libarary
-
服务端(Node)
- DOMPurify(npm 包)
- 用纯前端的方式,不适用服务器端拼接后返回(不使用服务端渲染)
- 字符串转义
替换字符 < > 为对应的 html 字符实体
value.replace(/</g, '<').replace(/>/g, '>')
- 使用 CSP
CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击
对 eval + inline script 直接拒绝
其他需要防范的情况:
CSRF 跨站请求伪造攻击
Cross-site request forgery
攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。也就是:
- 在用户不知情的前提下
- 利用用户权限(cookie)
- 构造指定 HTTP 请求,窃取或修改用户敏感信息
本质:利用 cookie 会在同源请求中携带发送给服务器 的特点,冒充用户。
GET 类型的 CSRF
比如一个网站的 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起请求
举例:
<img style="display:none;" src="https://bank.com/transfer?to=hacker&amount=100">
POST 类型的 CSRF
比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单
链接类型的 CSRF
比如在 a 标签的 href 属性里构建要给请求,然后诱导用户去点击
举例:
<a href="https://bank.com/transfer?to=hacker&amount=100">点我抽奖</a>
预防 CSRF 攻击
- 进行同源检测
服务器根据 http 请求头中的 orgin 和 referer 信息来判断请求是否为允许访问的站点
缺点是:referer 也可以伪造;搜索引擎的请求会被屏蔽(通常会设置为允许,但这也容易被利用)
比如 iframe 请求是同源的:
应该设置 X-Frame-Options 响应头部。
- 使用 CSRF Token 进行验证
服务器向用户返回一个随机数 Token,当网站再次发起请求时,在请求参数或者表单中加入这个 token,然后服务器对这个 token 进行验证。(必须保证令牌是由服务器动态生成的,而不是客户端生成)
这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题。
缺点:
- 网站中的所有请求都添加上这个 token,操作比较繁琐
- 一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。(这种情况可以通过改变 token 的构建方式来解决)
- 应该进行用户绑定,以免一个合法用户作为攻击者试探其他的越权功能
- 需要设置过期时间,否则被窃取则会一直被攻击
- 对 Cookie 进行双重验证
服务器在用户访问页面时,向请求域名注入一个 Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较来验证。
使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。
并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。
缺点:如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
- cookie 属性设置 Samesite
限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。
Samesite 一共有两种模式:
- 严格模式:cookie 在任何情况下都不能作为第三方 Cookie 使用
- 宽松模式:cookie 可以被 类型是 GET 且 会发生页面跳转 的请求使用
SameSite vs CORS
应该在 node 层用中间件防御各种 CSRF 攻击。
Injection 注入攻击
SQL Injection
- 读取请求字段
- 直接以字符串的形式拼接 SQL 语句
// 服务端
public async renderForm(ctx) {
const { username, form_id } = ctx;
const result = await sql.query(`
SELECT a, b, c FROM table
WHERE username = ${username}
AND form_id = ${form_id}
`);
ctx.body = renderForm(result);
}
// 浏览器端
fetch("/api", {
mothod: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username: "any; DROP TABLE table;"
})
})
// 执行的结果(删库)
SELECT xxx FROM xxx; DROP TABLE table;
还有其他注入:
-
CLI
-
OS command(命令行)
-
举例
// 服务器端 public async convertVideo(ctx) { const { video, options } = ctx.request.body; exec(`convert-cli ${video}-o ${options}`); ctx.body = "ok"; } // 浏览器端 fetch("/api", { method: "POST", body: JSON.stringify({ options: `' && rm -rf xxx` }) }); // 执行的结果 const command = `convert-cli video -o && rm -rf xxx` // ~~~~~~~~~~
-
-
SSRF(服务器伪造请求,不是注入,但原理类似)
- 举例:
读取和修改 举例:
防御
- 最小权限原则:不要轻易给 sudo 或 root 权限
- 建立允许名单 + 过滤:不要轻易允许使用 rm 命令
- 对 URL 类型参数进行协议、域名、ip等限制:拒绝访问内网
Dos
Denial of Service,服务器拒绝
通过某种方式(构造特定请求),导致服务器资源被显著消耗,来不及响应更多请求,导致请求挤压,进而雪崩效应。
基于贪婪正则表达式的 Dos
防御:
- Code Review:避免贪婪正则表达式
- 代码扫描 + 正则性能测试
- 禁止用户提供的正则表达式
DDoS
Distributed DoS,分布式的 DoS
短时间内,来自大量僵尸设备的请求流量,服务器不能及时完成全部请求,导致请求堆积,进而雪崩效应。
特点:不限制在域名,而是 ip
- 耗时的同步操作
- 数据库写入
- SQL join
- 文件备份
- 循环执行逻辑
SYN 洪水攻击 不返回 TCP 握手的第三次:
防御:
-
流量治理
- 负载均衡(过滤)
- API 网关(过滤)
- CDN(抗量)
-
快速扩容(抗量)
-
非核心服务降级(抗量)
传输层攻击
中间人攻击
详见 HTTP 的笔记中 HTTPS 部分
防御:HTTPS