在软件开发的阶段,应该考虑程序的安全性。本文总结一些在用node.js进行服务端开发时让服务更安全的知识。
validator.js
控制输入的数据,防止注入攻击
var validator = require('validator');
validator.isEmail('foo@bar.com'); //=> true
safe-regex
书写不恰当的正则表达式有可能造成Redos攻击(正则表达式拒绝服务攻击(Regular Expression Denial of Service)),攻击者可构造特殊的字符串,导致正则表达式运行会消耗大量的内存和cpu导致服务器资源被耗尽。
The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly (exponentially related to input size). An attacker can then cause a program using a Regular Expression (Regex) to enter these extreme situations and then hang for a very long time. ---摘自OWASP
const safe = require('safe-regex')
const re = /(x+x+)+y/
// 能跑死 CPU 的一个正则
re.test('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
// 使用 safe-regex 判断正则是否安全
safe(re) // false
Helmet
Helmet通过指定安全的http headers,防止跨站脚本攻击。
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet());
By default, Helmet sets the following headers:
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
express-rate-limit
对于单 IP 大量请求的暴力攻击,可以用express-rate-limit来进行限速
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
})
// Apply the rate limiting middleware to all requests
app.use(limiter)
csurf
CSRF (Cross Site Request Forgery): 跨站请求伪造。其原理是攻击者构造网站后台某个功能接口的请求地址,诱导用户去点击或者用特殊方法让该请求地址自动加载。
var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var express = require('express')
// setup route middlewares
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })
// create express app
var app = express()
// parse cookies
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser())
app.get('/form', csrfProtection, function (req, res) {
// pass the csrfToken to the view
res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function (req, res) {
res.send('data is being processed')
})
这个npm包现在被废弃了orz
HTTPS
Caddy 是一个自动支持 HTTP/2 的跨平台 Web 服务器,替代 Nginx,全站升级 https
CSRF 防范方式探讨
-
验证Referer
有一些博客写法是 referer.lastIndexOf(String.valueOf(stringBuffer)) != 0
lastIndexOf方法是指从后往前匹配子字符串
假设我们的正确Referer是example.com/ppp, 使用badguy.com?query=https://example.co… 就能绕过
所以使用lastIndexOf方法去验证Referer是错误的
最好的Referer头校验方式是,在增删改接口对Referer头进行严格检查,不允许为空,也必须以规定的字符串开头
-
设置CSRFToken
在一个客户端登录时服务端生成加密的CSRFToken,返回给客户端存储 此后每次请求服务端在请求头或请求体中携带CSRFToken 将服务端存储的CSRFToken与请求报文中的CSRFToken对比是否一致,以此来验证是来自合法用户的请求。
CSRFToken每次存在哪里?
前端: 存在localStorage或者sessionStorage里
后端: 如果是非分布式,可以存在服务器的内存中
如果是分布式集群,需要存储在Redis之类的公共存储空间(原因:在分布式环境下同一 个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿 到之前的HTTP请求存储在服务器中的Session数据)
如果需要保证系统性能的话,可以使用Encrypted Token Pattern方式(类似 JWT),CSRFToken只需要计算而不是存储
-
设置双重Cookie
在用户访问网站⻚面时,向请求域名注入一个Cookie,内容为随机字符串(例如 csrfcookie=v8g9e4ksfhw)
在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例 POST www.example.com/comment?csr…)
后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝
双重cookie局限性:
如果用户访问的网站为www.a.com,而后端的api域名为api.a.com。 那么在www.a.com下,前端拿不到api.a.com的Cookie,也就无法完成双重Cookie认证。 所以想要实施双重Cookie认证,Cookie的域设置在a.com,这样每个子域都可以访问。任何一个子域都可以访问修改a.com下的Cookie。 某个子域名存在漏洞被XSS攻击(例如upload.a.com)。虽然这个子域下并没有什么值得窃 取的信息。但攻击者修改了a.com下的Cookie。 攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再在www.a.com发起CSRF攻击。
因此,双重Cookie认证无法防御同个根域名下的CSRF攻击。如果有其他漏洞(例如XSS),攻 击者可以注入Cookie,那么该防御方式也会失效。
-
Set-Cookie响应头:SameSite
-
使用二次安全验证,如验证码等
Way 优点 缺点 Referer 开发工作量小 效果一般 CSRFToken 防御效果好 需要存储,占内存 双重 Cookie 实施简单 难以做到子域名的隔离,并且若有其他漏洞导致攻击者可 注入Cookie,这种方式的防御也会失效 JWT 防御效果好,如果设置合理能防御所有csrf攻击,对于后端无存储压力 JWT设置不当可能会导致更多安全问题,如JWT密钥易被猜解或爆破(secret泄漏)、JWT过期时间长 SameSite=Strict 防御效果好,实施简单 用户体验度差、浏览器兼容性 二次安全校验 防御效果好,实施简单 用户体验度差
XSS 防范方式探讨
XSS原理
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶 意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
本质:将恶意代码嵌入到当前网页中,浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
XSS分类
反射型xss
服务端没有对数据进行过滤、验证或者编码等处理直接返回前端可能引起的漏洞。
出现位置:交互的数据不会被存在数据库里面,一次性,所见即所得,一般出现在查询类页面。如网站的搜索框、登录注册等位置。
如用户在页面输入框中输入数据,通过GET或者POST方法向服务器端传递数据,输入的数据一般是放在URL的参数中,或者是form表单中,服务端直接把用户输入的数据返回至浏览器,浏览器解析这段带有XSS代码的数据后,最终造成XSS漏洞。这个过程就像一次反射。
存储型XSS
服务器端将用户输入的恶意脚本未经验证直接存储在数据库或者日志中,并且通过调用数据库的方式,每当页面被用户打开的时候执行,每当用户打开浏览器,恶意脚本就会执行。
出现位置:交互的数据会被存在数据库中,永久性存储,如常见于出现在留言板、注册、评论、发博客等任何可能插入数据库的地方。
存储型XSS攻击相比反射型的危害性更大,因为每当用户打开页面,恶意脚本都会执行,可造成蠕虫攻击、钓鱼等危害。
DOM型XSS
客户端的脚本程序可以通过DOM动态地检查和修改页面内容。当攻击者可以控制一些DOM对象、输入一些恶意JS代码,而客户端的脚本并没有对用户输入内容进行有效的过滤就传入一些执行危险操纵的函数如eval等或直接输出到页面时,就会导致DOM型XSS漏洞的存在。
<!DOCTYPE html>
<html>
<head>
<title>DOM XSS</title>
</head>
<body>
<script>
var pos=document.URL.indexOf("name=")+5;
document.write(decodeURI(document.URL.substring(pos,document.URL.length)));
</script>
</body>
</html>
其中document.URL属于外部输入sources,document.write属于敏感操作sinks,两个结合在一起时,如果网站前端JS代码将不可信的恶意外部输入数据利用敏感操作输出函数输出到页面中或者执行其他危险操作,导致恶意DOM型XSS。
Version:0.9 StartHTML:0000000105 EndHTML:0000001646 StartFragment:0000000141 EndFragment:0000001606
类似的sources
- document.URL
- document.URLUnencoded
- document.location(及其许多属性)
- document.referrer
- window.location(及其许多属性)
- location
- location.href
- location.search
- location.hash
- location.pathname
sinks
- document.open(…)
- window.open(…)
- document.write(…)
- document.writeln(…)
- element.innerHTML(…)
- eval(…)
- window.execScript(…)
- window.setInterval(…)
- window.setTimeout(…)
对比
| 分类 | 触发 | 输出 |
|---|---|---|
| 反射型 | 用户打开恶意构造的URL | 服务端 |
| 存储型 | 用户打开恶意构造的URL | 服务端 |
| DOM型 | 用户打开恶意构造的URL | 服务端 |
XSS绕过
标签绕过
问题原因:只转义部分标签,比如常用的
<script>
<a>
<svg>
等,可以利用其他标签进行绕过
关键字/属性绕过
问题原因:
- 只禁用部分on属性,如onerror、onload等
- 只禁用alert等关键字
可以利用其他关键字/属性进行绕过
伪协议绕过
问题原因:未禁用"javascript:;"
<a href="javascript:alert(123)">touch me</a>
<img src=x onerror="javascript:alert(123)">
空白符绕过
问题原因:js通常用分号结尾,当解析到完整语句并且行尾存在换行符的情况下就可以忽略掉分号,若解析确定不是完整语句,则会继续处理,直到语句结束或出现分号。因此碰到敏感词如javascript被过滤时,可以在其中添加空白符来绕过。 如:
<img src="java script:alert(‘xss‘);" width=100>
注释干扰绕过
问题原因:未对输入的注释字符串进行过滤
<scri<!--test-->pt>alert("hello world!")</scri<!--test-->pt>
输出
<script>alert("hello world!")</script>
// 单行注释
<!-- --!> 注释多行内容
<!-- --> 注释多行内容
<-- --> 注释多行内容
<-- --!> 注释多行内容
--> 单行注释后面内容
/* */ 多行注释
大小写绕过
问题原因:只过滤了如 script 标签,没有对其大写进行过滤,而浏览器对大写标签正常执行
<Script>alert("hello world!")<ScRipt>
注:只有标签可以大小写,关键字不可大小写,如Alert不会被浏览器识别
双写绕过
问题原因:对我们输入的标签进行替换为空,但只替换一次
<scri<script>pt>alert("hello world!")</scri</script>pt>
→
<script>alert("hello world!")</script>
编码绕过
问题原因:服务只进行了精准匹配关键词过滤,可以通过对关键词进行编码绕过
<a href="javascrip%74:alert(1)">11
CSS绕过
问题原因:CSS存在可以利用的特性
<div style="background-image:url(javascript:alert('sss'))">
<style>body{background-image:url(javascript:alert('saaa'))} ;</style>
<img style="xss:expression(alert('ssss'))">
<div style="list-style-image:url(javascript:alert('xxx'))">
<img style ="background-image:url(javascript:alert('sss'))">
外部引用含有XSS的CSS文件:
在www.xxx.com/1.css里写入通过link引入
p{
background-image:expression(alert('xss'))
}
XSS修复
预防/修复XSS主要有三种思路,相辅相成。
- HTML转义
- 前端渲染
- 浏览器内置安全策略
解决存储型和反射型XSS
存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的数据被内嵌到代码中,被浏览器所执行。预防这两种漏洞,有两种常见做法:
- 改成纯前端渲染,将代码和数据分隔
- 对 HTML 做充分转义
纯前端渲染
纯前端渲染的过程:
-
浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据;
-
然后浏览器执行 HTML 中的 JavaScript;
-
JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。这样浏览器可以区分即将执行的代码都是什么类型的代码,不会轻易被欺骗。但对于性能要求高,或有SEO需求的页面,我们仍然要面对拼接HTML的问题.
HTML转义
拼接HTML时对其进行转义。如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义(vue/react等主流框架已经避免类似问题,vue举例:不能在template中写script标签,无法在js中通过ref或append等方式动态改变或添加script标签)。这里推荐一个前端防止XSS攻击的插件: js-xss。其通过白名单保留部分标签和属性,再进行过滤。除了白名单内的标签和属性,白名单外的都要进行过滤。当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。
解决DOM型XSS攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。修复DOM型的思路主要是避免直接使用innerHTML、.outerHTML、document.write()等sink方法,把不可信的数据作为 HTML 插到页面上,在输出时可以先进行转义再输出,或者尽量使用.textContent、element.setAttribute("…","value") 等。
1)转义示例:
var ESAPI = require('node-esapi');
element.innerHTML = "<%=ESAPI.encoder().encodeForJS(ESAPI.encoder().encodeForHTML(untrustedData))%>";
//HTML encoding is happening in JavaScript
var ESAPI = require('node-esapi');
document.writeln(ESAPI.encoder().encodeForHTML(input));
2).textContent示例
<b>Current URL:</b> <span id="contentholder"></span>
<script>
document.getElementById("contentholder").textContent = document.baseURI;
</script>
浏览器内置防御策略:
1、httpOnly
对HTTP的Cookie字段设置httpOnly属性,浏览器禁止客户端的JavaScript访问带有httpOnly属性的Cookie。
2、X-XSS-Protection
在HTTP响应头添加X-XSS-Protection: 1;mode=block,启用浏览器对XSS过滤。
X-XSS-Protection响应头是IE、Chrome和Safari 的一个功能,当检测到XSS时,浏览器将停止加载页面,目前有四种取值:
- X-XSS-Protection: 0:禁止浏览器启用 XSS 过滤;
- X-XSS-Protection: 1:浏览器启用XSS 过滤(浏览器默认的值);
- X-XSS-Protection: 1;mode=block:启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载;
- X-XSS-Protection: 1; report=:启用XSS过滤【仅Chrome】, 如果检测到XSS,浏览器将清除页面并使用 CSP report-uri指令的功能发送违规报告(reporting-uri 就是发送违规报告的 URL 站点)。
3、Content Security Policy
CSP全称Content Security Policy,内容安全策略,是一个HTTP请求头Content-Security-Policy,本质上是建立浏览器允许动作白名单,Content-Security-Policy通过一系列的指令声明可以决定URL、多媒体资源、字体的加载策略、脚本的执行策略,有助于检测并缓解某些类型的攻击 如XSS和数据注入攻击。比如只允许本站脚本被浏览器接收,而别的域的脚本会失效,不被执行。目前主流浏览器如Chrome、Firefox、Safari、Edge都支持该特性。
开启 CSP 有两种方式:
1.通过 HTTP 头信息的 Content-Security-Policy 的字段:
Content-Security-Policy: script-src 'self'
2.通过网页的<meta>标签设置:
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
可以通过设置Content-Security-Policy default-src 'self'来仅允许加载本站的资源。
CSP绕过
必须特别注意 JSONP 的回调函数。 虽然加载的脚本来自当前域名,但是通过改写回调函数,攻击者依然可以执行恶意代码
<script src="/path/jsonp?callback=alert(document.domain)"></script>