基础前端安全

本篇博客仅以个人学习记录使用

XSS

XSS通常是由带有页面可解析内容的数据未经处理直接插入页面上解析导致的。需要注意的是,XSS分为存储型XSS、反射型XSS、MXSS(也叫DOM XSS)三种。这里区分不同类型主要是根据攻击脚本的引入位置:存储型XSS的攻击脚本常常是由前端提交的数据未经处理直接存储到数据库然后从数据库中读取出来后又直接插入到页面所导致的;反射型XSS的攻击可能是在网页URL参数中注入了可解析内容的数据而导致的,如果直接获取URL中不合法的并插入页面中则可能出现页面上的XSS攻击;MXSS则是在渲染DOM属性时将攻击脚本插入DOM属性中被解析而导致的。XSS主要的防范方法是验证输入到页面上所有内容来源数据是否安全,如果可能会含有脚本标签等内容则需要进行必要的转义。一般的做法是将所有可能包含攻击的内容进行HTML字符编码转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// HTML 字符转译编码
function htmlEncode(str) {
let s = '';
if(str.length == 0 ) return '';
s = str.replace(/&/g, '$amp;')
s = s.replace(/</g, '&lt;');
s = s.replace(/>/g, '&gt;');
s = s.replace(/ /g, '&nbsp;');
s = s.replace(/\'/g, '&#39;');
s = s.replace(/\"/g, '&quot;');
s = s.replace(/\n/g, '<br>');
return s;
}

//HTML 字符转译编码
function htmlDecode(str) {
let s = '';
if(str.length == 0 ) return '';
s = str.replace(/&amp;/g, '&');
s = s.replace(/&lt;/g, '<');
s = s.replace(/&gt;/g, '>');
s = s.replace(/&nbsp;/g, '');
s = s.replace(/&#39;/g, '\');
s = s.replace(/&quot;/g, '\"');
s = s.replace(/<br>/g, ''/n);
return s;
}

SQL注入攻击

SQL注入攻击主要是因为页面提交数据到服务器端后,在服务器端未进行数据验证就将数据直接拼接到SQL语句中执行,因此产生执行与预期不同的现象。主要防范措施是对前端网页提交的数据内容进行严格的检查校验。

1
2
3
4
5
6
let di = req.query['id'];
let sql = `select * from user_table where id=${id}`;

let data = exec(sql);
this.body = data;

例如以上实例,如果前端传入的id内容为”100 or name=%user%”, 那么查询出来的结果就不只是id=100的用户了,包含user字符用户名的用户内容也都会被查询出来,并且这些用户信息可能被输出,导致SQL注入的发生。所以这是我们需要对传入的id内容进行校验,检查是否包含非法内容。

CSRF

CSRF是指非源站点按照源站点额数据请求格式提交非法数据给源站点服务器的一种攻击方法。非源站点在取到用户登陆验证信息的情况下,可以直接对源站点的某个数据接口进行提交,如果源站点对该提交请求的数据未经验证,该请求可能被成功执行,这其实并不合理。通常比较安全的是通过页面token(令牌)提交验证的方式来验证请求是否为源站点页面提交的,来阻止跨站伪造请求的发生。

用户通过源站点页面可以正常访问源站点服务器接口,但是也有肯能被钓鱼进入伪站点来访问源服务器,如果伪站点通过第三方或用户信息拼接等方式获取到了用户的信息、直接访问源站点的服务器接口进行关键性操作(例如支付扣款或返回用户隐私信息等操作),此时如果源站点服务器未做校验防护,伪站点的请求操作就可以被成功执行。另一种情况则可能是盗刷源站点的登录等接口来暴力破解用户密码的情况,如果源站点不添加防护措施,用户信息就极可能被盗取,所以我们需要进行安全性验证。

我们在源站点服务请求调用时添加了对源站点的验证,使用服务器端实时返回加密的验证Token给源站点页面,在源站点页面提交时将Token一起带给服务器验证,而Token是不会被其他伪站点利用的。而非法的伪站点和盗刷的行为就可以被直接拒绝掉,这样就大大降低了CSRF发生的概率。所以在Web后端,我们常常会进行Token验证,其中一种形式是将页面提交到后台的验证Token与session临时保存额Token进行比较就可以实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//生成随机的 csrf验证Token,并返回给前端页面
this.session.csrf = md5(Math.random(0,1).toString()).slice(5,15);
this.body = yield render('user/login',{
scrf: ctx.session.csrf
})

//提交时验证Token是否与源站点的Token相同
let csrf = this.request.body['csrf'];
if(csrf !== this.session.csrf) {
res = {
code:403,
msg: '不明网站来源提交'
}
}else {
//正常提交后的逻辑处理
}
1
目前解决CSRF的最佳方式就是通过加密计算的Token验证,而Token除了通过session也可以使用HTTP请求头中Authorization的特定认证字段来传递。当然并不是说使用了Token,网站调用服务就安全了,单纯的Token验证防止CSRF的方式理论上也是可以被破解的,例如可以通过域名伪造和拉取源站点实时Token信息的方式来进行提交。另外,任何所谓的安全都是相对的,只是说理论的破解时间变长了,而不容易被攻击。很多时候要使用多种方法结合的方式来一起增加网站的安全性,可以结合验证码等手段大大减少盗刷网站用户信息的频率等,进一步增强网站内容的安全性。

请求劫持与HTTPS

现在除了正常的前后端脚本安全问题,网络请求劫持的发生也越来越频繁。网络劫持一般指网站资源请求在请求过程中因为认为的攻击导致没有加载到预期的资源内容。网络请求劫持目前主要分为两种:DNS劫持与HTTP劫持。

DNS劫持

DNS劫持通常是指攻击者劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致用户对该域名地址的访问由原IP地址转入到修改后的指定IP地址的现象,其结果就是让正确的网址不能解析或被解析指向令一网站IP,实现获取用户资料或者破坏原有网站正常服务的目的。DNS劫持一般通过篡改DNS服务器上的域名解析记录,来返回给用户一个错误的DNS查询结果实现。

HTTP劫持

HTTP劫持是指,在用户浏览器与访问的目的服务器之间所建立的网络数据传输通道中从网关或防火墙层上监视特定数据信息,当满足一定的条件时,就会在正常的数据包中插入或修改成为攻击者设计的网络数据包,目的是让用户浏览器解释“错误”的数据,或者以弹出新窗口的形式在使用者浏览器界面上展示宣传性广告或者直接显示某块其他的内容。而发生HTTP劫持时网站开发者一般无法通过修改网站代码程序等手段去进行防范。请求劫持的唯一可行的预防方法就是尽量使用HTTPS协议来访问目标网站。但是即使是使用了HTTPS也不是百分百的,仍然可以通过某些手段降低HTTPS至HTTP,然后进行HTTP劫持。

HTTPS协议通信过程

HTTPS协议是通过加入SSL(Secure Sockets Layer)层来加密HTTP数据进行安全传输的HTTP协议,同时启用默认的443端口进行数据传输。那么使用HTTPS是怎样保证浏览器和服务器之间数据安全传输的呢?我们需要先理解两个概念:公钥和私钥。

公钥(Public key)与私钥(Private key)是通过一种加密算法得到的密钥对(即一个公钥和一个与之匹配的私钥),公钥是密钥对中公开的部分,私钥则是非公开的部分。公钥常常用于会话加密、验证数字签名或者加密可以用相应私钥解密的数据。通过这种算法得到的密钥对保证是唯一的。使用这个密钥对的时候,如果用其中一个密钥加密一段数据,则必须用另一个密钥解密。比如用公钥加密数据就必须用私钥解密,如果用私钥加密也必须用公钥解密,否则解密将不会成功。我们以公钥加密方式为例,来看看HTTPS进行消息安全通信的整个过程。

客户端在需要使用HTTPS请求数据时,首先会发起连接请求,告诉服务器将建立HTTPS连接;服务器收到通知后首先自己先生成一个公钥并将它返回给客户端,如果是第一次请求,同时还要告诉客户端需要进行连接验证;如果需要验证,客户端接收到服务器公钥后开始发送验证请求,将一个特定的验证串使用服务器返回的公钥加密后形成密文发送给服务器,同时客户端也将自己生成的公钥发送给服务器;服务器获取到加密的报文和客户端公钥,先使用服务器私钥解密报文获得验证串,然后将验证串通过接收到的客户端公钥加密后返回给客户端,客户端再通过私钥解密验证串,判断是否为自己开始发送的验证串;如果正确,说明双方的连接是安全的,连接验证成功,客户端开始将后面的数据通过服务器返回的公钥不断加密发送给服务器,服务器也不断解密获取报文,并通过客户端的公钥加密响应的报文内容返回给客户端验证。这样就建立了HTTPS双向的加密传输连接。

在这种情况下,传输层传输的内容不会以明文的方式显示,而且HTTPS的请求只能被添加了对应数字证书的应用层代理拦截,因此第三方攻击者就无计可施了。通常我们要创建HTTPS服务,在服务端可以使用对应的模块来实现,例如在Node端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//引入HTTPS模块
const httpsModule = require('https');
const fs = require('fs');

//加载网站HTTPS 服务证书文件,证书一般需要注册申请
const https = httpsModule.Server({
key: fs.readFileSync('/path/to/server.key'),
cert: fs.readFileSync('/path/to/server.crt')
},function(req,res){
res.writeHead(200);
res.end("hello world")
})

//HTTPS 默认监听443端口
https.listen(443, function(err){
console.log("https listening on port:443");
})

当然如果使用Web框架,也可以通过更简单的方式创建一个HTTPS服务器。

const koa = require('koa');
const app = koa();

//同时监听多个端口
app.listen(80);
app.listen(443);

浏览器Web安全控制

除了HTTPS外,前端浏览器设置的安全性的控制还有很多,通过某些特定的head头配置,就可以完成浏览器端的安全性设置。下面看几个典型的安全消息头域设置。

X-XSS-Protection

这个head消息头设置主要是用来防止浏览器中的反射性XSS问题的发生,通过这种方式可以在浏览器层面增加前端网页的安全性

1
2
3
X-XSS-Protection:1;
mode=block 0 -关闭对浏览器的xss防护;1 -开始xss防护
mode=block 可以开启XSS防护并通知浏览器组织而不是过滤用户注入的XSS脚本
Strict-Transport-Security

Strict Transport Security(STS)是一种用来配置浏览器和服务器之间安全通信的机制,主要用来防止中间者攻击,因为它强制所有的通信都是用HTTPS,在普通的HTTP报文请求中配置STS是没有作用的,而且攻击者也能更改这些值。为了防止这样的现象发生,很多浏览器内置了一个配置STS的站点列表,在Chrome浏览器下可以通过访问Chrome://net-internals/#hsts查看浏览器中站点的STS列表,一般STS的配置实现如下。

1
2
3
4
max-age = 31536000;
includeSubDomains;
preload; -告诉浏览器将域名缓存到STS列表里面并且包含所有的子域名,并可支持预加载,时间是一年
max-age = 0 -告诉浏览器移除在STS缓存里的域名,或者不保存当前域名
Content-Security-Policy

我们简称它为CSP,这是一种由开发者定义的安全策略性声明,通过CSP所约束的规则设定,浏览器只可以加载指定可信的域名来源的内容(这里的内容可以是脚本、图片、iframe、font、style等等远程资源)。通过CSP协定,Web只能加载指定安全域名下的资源文件,保证运行时的内容总处于一个安全的环境中。

Access-Control-Allow-Origin

Access-Control-Allow-Origin是从Cross Origin Resource Sharing(CORS)中分离出来的。这个头部设置是决定一个通配符或域名来决定是单一的网站还是所有网站可以访问服务器的资源。需要注意的是,如果服务器端定义了通配符“ * ”,那么服务端的Access-Control-Allow-Credentials(是否允许请求时携带验证信息)选项就无效了,此时用户浏览器中的不同域Cookie信息将默认不会再服务器请求里发送(即如果需要实现带Cookie进行跨域请求,则要明确的配置允许来源的域,使用任意域的配置是不合法的)

1
Access-Control-Allow-Origin常常作为跨域共享设置的一种实现方式,其他常用的跨域手段还有:JSONP(JSON with Padding)、script标签跨域、window.postMessage、修改document.domain跨子域、window.name跨域和WebSocket跨越等。