总览
浏览器相关:
- XSS
- CSRF
- HTTPS
- CSP (内容安全策略, 可以禁止加载外域代码, 禁止外域提交等等)
- HSTS (强制客户端使用HTTPS与服务端建立连接)
- X-Frame-Options (控制当前页面是否可以被嵌入到Ifrmae中)
- SRI (subresource intergrity 子资源完整性, 前端可以用webpack-subresource-integrity插件, 在每个script上添加hash值, 校验加载的资源是否和当时打包生成的一致)
- Referrer-Policy (控制referer的携带策略)
Node(服务端)相关
- 本地文件操作相关:路径拼接导致的文件泄露
- ReDOS
- 时序攻击
- ip origin referrer等request headers的校验(在做爬虫应用的时候对此会有深刻的体会)
XSS
Cross-site scripting, 跨站脚本, 通常简称为XSS.
为什么简写为XSS而不是CSS? 因为CSS被广泛应用于样式的称呼里, 而Cross意味交叉, X字母正好是符合交叉的含义, 所以简称为XSS。
说白了就是攻击者想尽一切办法将可执行代码注入到网页中, 而恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
外在表现上, 都有哪些攻击场景?
- 评论区植入js代码(即可输入的地方植入js代码)
- url上拼接js代码
- TIPS: 有点同学可能觉得在这种场景下, 用户能输入的代码长度有限, 根本构不成什么威胁? 然而攻击者是可以通过引入外部脚本来实现复杂攻击的.
具体从技术角度上分析, 都有哪些xss攻击的类型呢?
1、 存储型 Server
- 场景:常见于带有用户保存数据的网站功能,攻击者通过可输入区域来注入恶意代码, 如论坛发帖、商品评论、用户私信等。
- 攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中
- 用户打开目标网站时,服务端将恶意代码从数据库中取出来,拼接在HTML中返回给浏览器(因为用户之间是可以相互看到帖子、评论等的)
- 用户浏览器在收到响应后解析执行,混在其中的恶意代码也同时被执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户行为,调用目标网站的接口执行攻击者指定的操作。
2、 反射型 Server
与存储型的区别在于,存储型的恶意代码通过可输入区域, 存储在数据库中,而反射型的恶意代码拼接在URL上。 由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
- 场景:通过 URL 传递参数的功能,如网站搜索、跳转等。
- 攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站的接口执行攻击者指定的操作。
3、 Dom型 Browser
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
- 场景:通过 URL 传递参数的功能,如网站搜索、跳转等。
- 攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站的接口执行攻击者指定的操作。
简单模拟一下Dom型XSS攻击?
- index.html
1<!DOCTYPE html>2<html lang="zh">3 <head>4 <meta charset="UTF-8" />5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />6 <meta7 name="viewport"8 content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"9 />10 <title>XSS</title>11 </head>12 <body>13 <a href="">跳转到新地址</a>14 </body>15 <script src="type-dom.js"></script>16</html>
- type-dom.js
1const a = document.getElementsByTagName('a')[0];2const queryObject = {};3const search = window.location.search;4search.replace(/([^&=?]+)=([^&]+)/g, (m, $1, $2) => (queryObject[$1] = decodeURIComponent($2)));56a.href = queryObject.redirectUrl;
- 打开当前index.html, 添加参数redirectUrl访问
比如
redirectUrl=javascript:alert('哈哈哈笨蛋!!')
点击a链接发现已经被xss攻击了.
长度有限制
比如我们写这样一段脚本, 目的是创建一个script标签并且引入remote.js文件, 将这一串代码作为redirectUrl的值访问链接.
1var script = document.createElement('script');2script.type = 'text/javascript';3script.async = true;4script.src = 'remote.js';5var s = document.getElementsByTagName('script')[0];6s.parentNode.insertBefore(script, s);
新建remote.js文件, 来尝试耗性能的代码.
1let count = 0;2console.log(window.navigator.userAgent);34while(count++ < 100) {5 console.log('我要通过巨量运算把你搞崩溃')6}
- 而如果我们网站的cookie里有重要的用户信息, 那么攻击者是否就可以通过document.cookie获取到了?\
比如我们在原来的网站脚本type-dom.js里设置一下cookie, 然后在攻击脚本remote.js里获取.
document.cookie="name=john12313";
console.log(document.cookie);
但是聪明的同学应该也想到了, 只要我们给重要的cookie设置了http-only, 就算被dom xss攻击了, 攻击者也造不成大的影响.
- 再来试试访问url就直接出发的xss攻击.
type-dom.js
1document.write(queryObject.name)
添加参数访问url,
name=<script>window.alert(1)</script>
- 可以试一下这个网站, https://alf.nu/alert1
如何防范XSS攻击呢?
主旨:防止攻击者提交恶意代码,防止浏览器执行恶意代码
对数据进行严格的输出编码:如HTML元素的编码,JS编码,CSS编码,URL编码等等
- 避免拼接 HTML;Vue/React 技术栈,避免使用 v-html / dangerouslySetInnerHTML
CSP HTTP Header,即 Content-Security-Policy(不支持CSP的旧版浏览器可以设置X-XSS-Protection)
- 增加攻击难度,配置CSP(本质是建立白名单,由浏览器进行拦截)
- Content-Security-Policy: default-src ‘self’ -所有内容均来自站点的同一个源(不包括其子域名)
- Content-Security-Policy: default-src ‘self’ *.trusted.com -允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同)
- Content-Security-Policy: default-src https://john.com -该服务器仅允许通过HTTPS方式并仅从john.com域名来访问文档
可以做到很多事情,比如: 禁止加载外域代码,防止复杂的攻击逻辑。 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
输入验证:比如一些常见的数字、URL、电话号码、邮箱地址等等做校验判断
开启浏览器XSS防御:Http Only cookie,禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
验证码
CSRF
Cross-site request forgery, 跨站请求伪造.
攻击者诱导受害者进入恶意网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
攻击步骤
- 受害者登录 a.com,并保留了登录凭证(Cookie)
- 攻击者引诱受害者访问了b.com
- b.com 向 a.com 发送了一个请求:a.com/act=xx浏览器会默认携带a.com的Cookie
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
- a.com以受害者的名义执行了act=xx
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作
攻击类型
- GET型:如在页面的某个 img 中发起一个 get 请求
<img src="http://bank.example/withdraw?name=xxx&amount=xxxx" />
- POST型:通过自动提交表单到恶意网站
1<form action="http://bank.example/withdraw" method=POST>2 <input type="hidden" name="account" value="john" />3 <input type="hidden" name="amount" value="10000" />4 <input type="hidden" name="for" value="hacker" />5</form>6<script> document.forms[0].submit(); </script>
- 链接型:需要诱导用户点击链接
1<a href="http://bank.example/withdraw?name=xxx&amount=xxxx" taget="_blank">2错过再等一年!!!快来看看3</a>
如何防范CSRF的攻击呢?
首先咱们通过上面的举例可以知道, CSRF一般都是发生在第三方域名, 攻击者也无法获取到Cookie信息, 只是可以利用浏览器机制去使用Cookie.
所以咱们可以针对这两点来看防范策略:
阻止第三方域名的访问
- Cookie SameSite
SameSite有3个值: Strict, Lax和None
- Strict:浏览器会完全禁止第三方cookie。比如a.com的页面中访问 b.com 的资源,那么a.com中的cookie不会被发送到 b.com服务器,只有从b.com的站点去请求b.com的资源,才会带上这些Cookie
- Lax:在跨站点的情况下,从第三方站点链接打开和从第三方站点提交 Get方式的表单这两种方式都会携带Cookie。但如果在第三方站点中使用POST方法或者通过 img、Iframe等标签加载的URL,这些场景都不会携带Cookie
- None:任何情况下都会发送 Cookie数据
- 同源检测
通过检测request header中的origin referer等, 来确定发送请求的站点是否是自己期望中站点.
比如referer, 我们可以在服务端去判断referer是否来自可信域, 同样也可以做一些referer发送时的设置:
对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer。例如:aaa.com引用bbb.com的资源,不会发送Referer(服务端在接收到没有referer的请求时就不做响应)
这就提到了Referrer-Policy这个请求头. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy, 控制什么情况下应该携带/不携带referer
提交请求时附加额外信息
因为攻击者无法通过csrf来获取本域下的cookie等信息, 所以可以利用这一点来做防范.
- CSRF Token
- 用户打开页面的时候, 服务器利用加密算法给当前用户生成一个ToKen
- 每次页面加载时, 前端把获取到的Token加到所有的能发请求的html元素上, 比如form, a
- 每次前端发起请求, 都携带Token参数
- 服务端每次接收请求, 都校验Token的有效性.
- 双重Cookie
- 用户访问网站的时候, 服务器向浏览器注入一个额外的cookie, 内容随便, 比如csrfcookie=johnxzxfasdfasfew
- 每次前端发起请求, 都在请求上拼接上csrfcookie这个参数, 参数值就从cookie里获取
- 服务端每次收到请求, 就去校验请求参数里的值和cookie里的值是否一致
但是这种方式安全性不如CSRF Token. 咱们来看一下原因:
- 如果前端域名和服务端域名不一样, 比如前端 fe.a.com, 后端 rd.a.com, 那如果服务端希望前端能拿到csrfcookie, 就只能把这个cookie设置到a.com域下, 并且不能设置为http-only
- 那么a.com的每个子域名就都可以获取到这个cookie
- 一旦某个子域名遭到xss攻击, cookie就很容易被窃取或者被篡改.
- 攻击者利用篡改或者窃取的csrfcookie, 就可以攻击fe.a.com了
Node(服务端)相关的安全问题
本地文件操作
比如我们提供一个静态服务, 通过请求的参数url来返回给用户或者前端想要的资源.
- 新建文件node/static-sever-dangerous.js
1const fs = require('fs');2const http = require('http');3const path = require('path');45http6 .createServer(function (req, res) {7 const file = path.join(__dirname, 'static', req.url);89 fs.readFile(file, function (err, data) {10 if (err) {11 res.writeHead(404, { "Content-Type": "text/plain;charset=utf-8" });12 res.end('找不到对应的资源');13 return;14 }15 res.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });16 res.end(data);17 });18 })19 .listen(8080);20 console.log('server listening on port 8080');
- 新建文件夹static, 里面随便放点文件, 提供给外界请求
比如 test.json
1{2 "name": "john"3}
- 启动服务, 本地请求url
http://localhost:8080/test.json
http://localhost:8080/test1.json
没有任何问题对吧? 存在的资源正常返回了, 不存在的资源返回404了.
- 请求一个不一样的url
http://localhost:8080/?/../../../questions.md
http://localhost:8080/?/../../../private.js
同级新建一个private.js, 里面写上一些私密信息
1// 这是一个私密文件
可以看到, 攻击者可以通过拼接相对路径, 一次次猜你项目的结构, 并且可以访问到你服务器上的各种资源!
- 怎么解决这个问题?
很多node框架都自带插件来屏蔽这个问题的发生
比如express.static, koa-static, 当然也有第三方包支持(resolve-path)
咱们用resove-path来解决一下这个问题.
- npm init
- yarn add resolve-path —registry=https://registry.npm.taobao.org
- 新建 static-sever-secure.js 文件
1const fs = require('fs');2const http = require('http');3const path = require('path');4// 引入resolve-path5const resolvePath = require('resolve-path');67http8 .createServer(function (req, res) {9 try {10 // 先获取rootDir11 const rootDir = path.join(__dirname, 'static');12 // 调用resolvePath13 const file = resolvePath(rootDir, req.url);1415 fs.readFile(file, function (err, data) {16 if (err) {17 // 把错误抛出去18 throw err;19 }20 res.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });21 res.end(data);22 });23 } catch(e) {24 // catch住错误, 防止服务直接挂掉25 console.log(e);26 res.writeHead(404, { "Content-Type": "text/plain;charset=utf-8" });27 res.end('找不到对应的资源');28 }2930 })31 .listen(8081); // 改成8081端口32 console.log('server listening on port 8081');
- 可以看到resolve-path的源码对path做了严格的限制.
截图服务
比如咱们来实现一个简单的截图服务
- 安装好必要的npm包
yarn add puppeteer-chromium-resolver koa --registry=https://registry.npm.taobao.org
- 新建 screen-shot.js
1(async () => {2 const PCR = require('puppeteer-chromium-resolver');3 const stats = await PCR();45 const browser = await stats.puppeteer6 .launch({7 headless: true,8 args: ['--no-sandbox'],9 executablePath: stats.executablePath,10 })11 .catch(function (error) {12 console.log(error);13 });1415 const Koa = require('koa');16 const app = new Koa();1718 app.use(async ctx => {19 const { url } = ctx.query;2021 // 这里演示不合理的Url校验22 if (!url) {23 ctx.body = 'Invalid url';2425 return;26 }2728 ctx.set('Content-Type', 'image/png');2930 const page = await browser.newPage();31 await page.goto(url, { waitUntil: 'networkidle2' });3233 ctx.body = await page.screenshot({ encoding: 'binary', type: 'png' });3435 await page.close();36 });37 app.listen(8083);3839 console.log('server listening on port 8083');40 })();
- 访问url
- http://localhost:8083/?url=http://localhost:8080/test.json
这里咱们传入url为之前启动的不安全的静态服务. 可以看到我们直接截图了对应的资源
- http://localhost:8083/x?url=file:///etc/zshrc
这里咱们传入url为文件系统的zshrc, 可以发现我们直接截图了系统文件的内容.
ReDOS
新建redos.js, 分别看一下这三个例子的执行时间
1console.time('case-1');2// 能够匹配成功3/A(B|C+)+D/.test('ACCCCCCCCCCCCCCCCCCCCCCCCCCCCD');4console.timeEnd('case-1');56console.time('case-2');7// 不能匹配成功8/A(B|C+)+D/.test('ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX');9console.timeEnd('case-2');1011console.time('case-3');12// 不能匹配成功13/A(B|C+)+D/.test(`A${'C'.repeat(30)}X`);14console.timeEnd('case-3');
可以看到, 当不能匹配成功的时候, 每多一个字符, 所消耗是时间都是指数增长的.
因为咱们服务器经常会有正则去匹配一些传入的参数, 所以攻击者就可以利用正在表达式的这个特性, 来一直占用服务器运算资源, 造成服务器宕机.
具体原理可以看这篇文章:https://snyk.io/node-js/connect
正则表达式一般情况下会去匹配第一种可能性, 比如一个正确的字符串 ACCCD, 那么直接匹配到最后发现成功了, 耗时就很短.
而比如ACCCX这样一个字符, 每当一次匹配不成功, 就会尝试回溯到上一个字符, 看看能不能有其他的组合来匹配到这个字符串.
比如刚才说的ACCCX这样一个字符串, 会去尝试匹配四种不同的”C”字母组合来与其他字母组合, 看是否符合条件.
- CCC
- CC+C
- C+CC
- C+C+C.
可以在写完正则后去这个网址测试一下 https://regex.rip/? 测试是否会遭到reDos.
时序攻击
这种攻击方式在咱们编码过程中可能很少见, 看下面这个例子
比如咱们要匹配接收的数组和咱们定义好的数组是否完全一致, 如果一致才可以进行之后的操作.
1function compareArray(realArray, userInputArrary) {2 for (let i = 0; i < realArray.length; i++) {3 if (realArray[i] !== userInputArray[i]) {4 return false;5 }6 }7 return true;8}
这样写有没有什么问题? 逻辑上没有任何问题, 但是有安全问题.
因为我们如果判断到一个字符不相等, 就提前返回了!!
这给攻击者提供了一种方式, 就是根据服务器的响应时间来碰撞出realArray的值.
比如realArray = [2,3,6,1]
攻击者开始尝试 inputArray = [1,2] inputArray = [1,3]
发现这两种的响应时间几乎一致, 则可以认为第一个数字不是1
inputArray = [2, 2]
发现响应时间延长了, 则可以认为第一个数字是2.
长而久之, 就可以碰撞出真实的realArray了.