
网络安全是用于保护关键系统和敏感信息免遭数字攻击的实践。本文例举了常见的网络安全问题及在前后端防范的方法。
Cross-Site Scripting(跨站脚本攻击)简称XSS,是一种代码注入攻击。攻击者通过在目标网站上注入脚本,让它运行在用户浏览器上,利用这些脚本可以获取用户的信息例如cookie。XSS的本质就是恶意代码没有经过处理,跟网站正常代码混在一起。下面说说常见的XSS类别以及如何防护。
存储型是指攻击者将恶意代码提交到服务器的数据库中,当普通用户正常打开网站浏览时,服务器从数据库中取出并拼接到HTML中返回给浏览器,这时就加载到了恶意代码。这类攻击主要是由于未转译用户保存数据而直接显示引起的。
用户提交信息
<img src="xxx" onerror="alert('XSS Attack')">
服务端使用模板
<div id="comment"><%= comment.body %></div>
或前端使用数据
const $comment = document.querySelector("#comment");
$comment.innerHTML = comment.body;
首先我们需要明确是否需要 html 片段,如果可能的话将他们当做文本来使用。比如在模板中使用<%-而不是<%= (示例说明,上方是 ejs,根据不同模板而定) ;在客户端中尽可能使用textContent而不是innerHTML。
如果确实需要 html 片段来显示则需要特别注意,因为富文本是最容易受到攻击的。我们可以为这个富文本建立一个白名单,滤出允许出现的标签和属性然后再使用它。可以借助xss库来完成这件事、
注意:在 HTML5 之后,innerHTML 不会执行 script 脚本。
npm install xss
import filterXSS from "xss";
const config = {
whiteList: {
span: [],
p: [],
div: [],
h1: [],
h2: [],
h3: [],
img: ['alt', 'src'],
a: ['title', 'href'],
},
// 过滤所有非白名单的标签保留内容
stripIgnoreTag: true
// 过滤script标签并丢弃内容
stripIgnoreTagBody: ["script"]
}
// 客户端处理
$comment.innerHTML = filterXSS(comment.body, config);
// 服务端处理
ejs.renderFile(
filename,
{ ...comment, body: filterXSS(comment.body) },
function(err, str){
if (err) {
console.log(err);
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(str);
res.end();
}
}
);
反射型是指攻击者创建出特出的URL,而开发人员未对URL进行处理,直接将URL参数进行拼接HTML使用,当用户打开这个URL时就加载到了恶意代码。这类攻击主要是由于未转译URL数据而直接使用引起的。
用户提交信息
<img src="xxx" onerror="alert('XSS Attack')">
服务端使用模板
<input placeholder="搜索" id="search">
<div class="result">
<h2>
搜索关键词:
<span id="keyword"><%= getParameter("keyword") %></span>
</h2>
</div>
或客户端使用数据
const params = Object.fromEntries(new URLSearchParams(location.search));
const $keyword = document.querySelector("#keyword");
$search.innerHTML = params.keyword;
这里处理方式与存储型一样不在赘述。
DOM型是指前端JavaScript代码不严谨,将数据直接设置为DOM属性,而有些属性字符串是能作为代码运行。比如比较常见的就是a标签的href属性,还有一些内联事件处理器如onload、onerror、onclick等等。
用户提交信息
javascript:alert('XSS Attack')
客户端使用数据
const $recom = document.querySelect("a#recom");
$recom.setAttribute("href", data.recom);
这类攻击我们需要将属性转译,或者忽略掉恶意代码,可以通过检测是否是正确资源链接,如果不是则删除。为了防止漏掉某项,还是建议找第三方成熟库来做这件事,比如我们可以通过xss库的内置方法safeAttrValue来做这件事。
import filterXSS from "xss";
const link = filterXSS.safeAttrValue("a", "href", data.recom);
$recom.setAttribute("href", link);
"网页安全政策"(Content Security Policy,缩写 CSP)是厂商们推出的防护XSS的策略。本质上就是白名单制度,告诉浏览器哪些资源可加载,可执行,不过这个白名单的实现和执行是在浏览器完成的。我们只需要在http响应头中配置Content-Security-Policy字段,在这个字段中配置。
Cross-site request forgery(跨站请求伪造)简称CSRF,是一种利用用户凭证,进而伪装成用户执行非本意的操作的攻击方法。典型的CSRF攻击就是用户登录了 A 网站,然后打开了 B 网站,B 网站往 A 网站发送请求,由于 A 网站未做防护,接受了这个请求通过凭证认证用户并处理。
CSRF有攻击在第三方网站执行,攻击利用用户的凭证数据,无法直接窃取凭证而是伪装等特点。我们可以针对这个特点指定防护。
既然CSRF是来自第三方网站的,那我们在在请求中拦截不安全域名不是就可以了。在http协议中可以使用origin和referer请求头可以获取请求来源域名。
| origin | referer | |
|---|---|---|
| 描述 | 指示了请求来自于哪个站点 | 当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面(如搜索引擎)里的链接进入的 |
| 携带值 | 服务器名称,并不包含任何路径信息。 | 服务器名称,并不包含任何路径信息。 |
| 发送 | CORS (跨域请求) 请求或非GET和HEAD请求。 |
非http跳转https |
| 特殊 | 由于同源定义不同IE11跨站不会携带;302重定向时也不会携带 |
在html中对于资源可以指定是否发送referer,参见阮一峰博文 |
我们在后端拦截器中可以获取来源,如果是可信地址就放行,不可信来源则档回。
但是有些情况我们还是需要特殊处理,如果两个值都获取不到时如果没做二次确认(如下方确认处理)最好拦截认为是不可信的。
如果不是可信来源全部拦截还会造成一个问题,比如从google一类搜索引擎进入的,referer值就是www.google.com也会被当成CSRF拦截下来,那我们的页面就打不开了,我们需要为GET请求且Accept为text/html一类的请求放行。但是这又造成另一个问题,如果一些接口是通过GET携带参数来实现功能的话,这类接口也防御不了CSRF,这也是GET为什么安全性比POST弱的其中一个原因。
在chrome51之后浏览器对cookie新增了一个会影响跨站访问携带凭证的属性 SameSite,下面是这个属性的值:
Strict,严格的,完全禁止第三方cookie,只有当前网页URL与请求目标一致才会携带。可能造成不好的用户体验,比如从一个github连接点击进去不会携带cookie会被判断为未登录Lax,稍微宽松,大多数不允许第三方cookie,从顶级导航到目标地址的Get (链接,预加载请求,GET表单) 除外。None允许第三方跨站访问携带cookie,该属性有一个要求,必须同时为cookie设置Secure (该值指明cookie只能在https请求中被携带) ,第三方必须是https协议的源。下图是这个属性的兼容性

将cookie的SameSite设置为Strict就很安全了,任何跨域都不能携带cookie,但是体验下非常不好。一般我们会设置为Lax,但是安全性也就降低了,从导航到目标地址的Get也会携带cookie。所以这个方法也没解决通过GET携带参数来实现功能的CSRF的防御。
另外在网上绝大多数都说这个属性是不支持子域的,其实错的。这个属性是描述跨站时cookie的携带方式,而不是跨域。也就是说只要设置好了domain,子域也能共享cookie,对于ajax需要设置withCredentials。
既然GET请求来做功能和SameSite都无法防御,那我们索性将他们放行。然后加多一层验证,我们在服务端生成一段唯一的随机数并设置到cookie中,前端在请求前从cookie中获取到这个随机数,然后加入到请求体或请求path中。后端收到请求时第二层验证验证此参数与cookie中的是否一致。
这个防御主要是针对CSRF是利用用户的凭证数据,无法直接窃取凭证来实施的,这样我们就解决了GET写单参数实现功能的CSRF防御了,但是这个方式也有一定的弊端。
如果有多个子域,我们需要将这个cookie种在主域,这样每个子域才能访问得到它,难以做到子域隔离。如果子域有一个站点存在XSS漏洞被攻击,那攻击者就能拿到这个随机数,所有站点都有可能被CSRF攻击。
这种方式也是类似双重cookie的原理,加多一层随机数验证。token是后端通过一段唯一的字符串 (一般是随机数与时间戳组合) 加密后生成,然后将token存储起来,并通过模板渲染嵌入到html,或者开放更新并返回token接口 (一般指登录接口) ,然后客户端在请求时携带上token的参数,后端在拦截器中验证token是否有效与是否过期,如果有效则放行,无效则拒绝。
由于CSRF一般是在第三方进行,无法获取到token的,只要页面没有XSS漏洞泄露Token,那么CSRF攻击就无法成功。
另外在前后端分离使用token认证来代替cookie认证用户身份最主流的方式。原理就在于服务端可以通过token关联用户信息,只要携带上token就可以知道是哪个用户,而前端获取到token后缓存到localStorage或sessionStorage中,并将token设置到全局请求头中,这样一来就可以代替cookie了,并且Storage的数据并不会跟随请求而被携带,能有效防范CSRF。
在敏感接口比如转账,更改密码,注销账号等,还可以使用二次确认,比如再次输入密码或手机验证码也能防止CSRF攻击,而且比上面几种方式更安全。
在http数据提交给TCP层之后,会经过用户电脑、路由器、运营商、服务器,这中间每一个环节,都不是安全的。因为http采用的是明文传输,攻击者可以通过中间环节对数据窃取、伪造、篡改这就是中间人攻击。
为了防范中间人攻击,我们需要对传输的数据进行加密,确保私密性和完整性 (防止修改) 。而https就是干这件事的,在说明https的工作方式之前我们先了解一些相关词汇。
对称加密:对称算法是内容加密的一类算法。它有一个秘钥,通过秘钥解开加密内容。
非对称加密:非对称算法是内容加密的一类算法。它有两个秘钥,公钥与私钥。公钥是公开给所有人的,私钥是私密的,只有持有者知道。通过公钥加密后的内容只有通过私钥才能解密出来。非对称算法的安全性很高,但是因为计算量庞大,比较消耗性能。非对称加密算法可逆(可解密)。
认证:认证是指不在意传输内容被看到,只需要确保内容是完整的,传输方式正确的。
数字签名:数字签名是指发送方在发送源内容的同时也发送一份对源内容进行不可逆算法加密(签名)。接收方也通过相同算法对发送内容进行加密,查看加密后的内容是否与发送方一致,如果一致则内容可信。
证书中心,CA 认证,数字证书:在一些情况下,接收方的公钥可能被偷偷替换成其他劫持者的公钥,此时劫持者就可以与接收方通话,所以就需要 CA 认证。CA 会用自己的私钥给要认证的公钥及其他一些信息进行签名,然后发送给持有者,持有者在发送数据时将 CA 认证后的数字签名发送给接收方,接收方通过 CA 给的公钥验签,验签通过认为是安全的公钥。
https通讯的过程443端口请求,并携带了浏览器支持的加密算法和 hash 算法前 5 步其实就是https握手过程,主要是认证服务端证书的合法性。因为非对称计算量比较大, 整个通讯过程只会用到一次非对称加密算法(主要是用来保护传输客户端生成用于对称加密随机数秘钥)。后续内容的加解密都是用一开始约定好的对称加密算法进行的。
通过https的混合加密方式 (非对称加密和对称加密结合) 大大增加了中间人的攻击成本。但是https不是绝对安全的,下面例举一些情况
虽然不是绝对安全,但是已经是在现有架构下最好的解决方案了。
完结撒花
©2021 - bill-lai 的小站 -站点源码