在软件开发的阶段,应该考虑程序的安全性。本文总结一些在用node.js进行服务端开发时让服务更安全的知识。

validator.js

控制输入的数据,防止注入攻击

var validator = require('validator');

validator.isEmail('foo@bar.com'); //=> true
safe-regex

ReDos

书写不恰当的正则表达式有可能造成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

CSRF 防范方式探讨
  1. 验证Referer有一些博客写法是 referer.lastIndexOf(String.valueOf(stringBuffer)) != 0lastIndexOf方法是指从后往前匹配子字符串假设我们的正确Referer是https://example.com/ppp, 使用http://badguy.com?query=https://example.com/ppp 就能绕过所以使用lastIndexOf方法去验证Referer是错误的最好的Referer头校验方式是,在增删改接口对Referer头进行严格检查,不允许为空,也必须以规定的字符串开头
  2. 设置CSRFToken在一个客户端登录时服务端生成加密的CSRFToken,返回给客户端存储 此后每次请求服务端在请求头或请求体中携带CSRFToken 将服务端存储的CSRFToken与请求报文中的CSRFToken对比是否一致,以此来验证是来自合法用户的请求。CSRFToken每次存在哪里?前端: 存在localStorage或者sessionStorage里后端: 如果是非分布式,可以存在服务器的内存中如果是分布式集群,需要存储在Redis之类的公共存储空间(原因:在分布式环境下同一 个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿 到之前的HTTP请求存储在服务器中的Session数据)如果需要保证系统性能的话,可以使用Encrypted Token Pattern方式(类似 JWT),CSRFToken只需要计算而不是存储
  3. 设置双重Cookie在用户访问网站⻚面时,向请求域名注入一个Cookie,内容为随机字符串(例如 csrfcookie=v8g9e4ksfhw)在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例 POST https://www.example.com/comment?csrfcookie=v8g9e4ksfhw)后端接口验证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,那么该防御方式也会失效。
  4. Set-Cookie响应头:SameSite
  5. 使用二次安全验证,如验证码等

Way

优点

缺点

Referer

开发工作量小

效果一般

CSRFToken

防御效果好

需要存储,占内存

双重 Cookie

实施简单

难以做到子域名的隔离,并且若有其他漏洞导致攻击者可 注入Cookie,这种方式的防御也会失效

JWT

防御效果好,如果设置合理能防御所有csrf攻击,对于后端无存储压力

JWT设置不当可能会导致更多安全问题,如JWT密钥易被猜解或爆破(secret泄漏)、JWT过期时间长

SameSite=Strict

防御效果好,实施简单

用户体验度差、浏览器兼容性

二次安全校验

防御效果好,实施简单

用户体验度差

HTTPS

Caddy 是一个自动支持 HTTP/2 的跨平台 Web 服务器,替代 Nginx,全站升级 https