[node] nodejs中的web安全
配置管理
安全HTTP头
- Strict-Transport-Security 强制实施与服务器的安全(HTTP over SSL / TLS)连接
- X-Frame-Options 提供点击劫持保护
- X-XSS-Protection 支持在最新的Web浏览器中内置的跨站点脚本(XSS)过滤器
- X-Content-Type-Options 可防止浏览器从声明的内容类型中嗅探响应
- Content-Security-Policy 可防止各种攻击,包括跨站点脚本和其他跨站点注入
可使用helmet模块快捷设置
const express = require('express')
const helmet = require('helmet')
const app = express()
app.use(helmet())
// ...
使用nginx时的配置
# nginx.conf
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "default-src 'self'";
客户端的敏感数据
注意不要在客户端代码中存放敏感数据
认证
暴力(破解)保护
可以使用ratelimiter模块限制登录频率
var ratelimit = require('koa-ratelimit');
var redis = require('redis');
var koa = require('koa');
var app = koa();
var emailBasedRatelimit = ratelimit({
db: redis.createClient(),
duration: 60000,
max: 10,
id: function (context) {
return context.body.email;
}
});
var ipBasedRatelimit = ratelimit({
db: redis.createClient(),
duration: 60000,
max: 10,
id: function (context) {
return context.ip;
}
});
app.post('/login', ipBasedRatelimit, emailBasedRatelimit, handleLogin);
会话管理
安全使用cookie的重要性不容低估:特别是在动态Web应用程序中,需要在HTTP等无状态协议中维护状态。
Cookie标志
- secure - 此属性告诉浏览器仅在通过HTTPS发送请求时才发送cookie。
- HttpOnly - 此属性用于帮助防止跨站点脚本等攻击,因为它不允许通过JavaScript访问cookie。
Cookie范围
- domain - 此属性用于与请求URL的服务器的域进行比较。如果域匹配或者它是子域,则接下来将检查路径属性。
- path - 除了域之外,还可以指定cookie有效的URL路径。如果域和路径匹配,则cookie将在请求中发送。
- expires - 此属性用于设置持久性cookie,因为cookie在超过设置日期之前不会过期
var cookieSession = require('cookie-session');
var express = require('express');
var app = express();
app.use(cookieSession({
name: 'session',
keys: [
process.env.COOKIE_KEY1,
process.env.COOKIE_KEY2
]
}));
app.use(function (req, res, next) {
var n = req.session.views || 0;
req.session.views = n++;
res.end(n + ' views');
});
app.listen(3000);
CSRF
跨站点请求伪造是一种攻击,迫使用户在他们当前登录的Web应用程序上执行不需要的操作。这些攻击专门针对状态更改请求,而不是数据被盗,因为攻击者无法访问看到对伪造请求的回应。
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();
// 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');
});
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
Favorite color: <input type="text" name="favoriteColor">
<button type="submit">Submit</button>
</form>
数据验证
XSS
一种是跨站点脚本的反射版本,另一种是存储。
当攻击者使用特制链接将可执行JavaScript代码注入HTML响应时,会发生反射的跨站点脚本(Reflected Cross site scripting)。
当应用程序存储未正确过滤的用户输入时,将发生存储的跨站点脚本(Stored Cross site scripting)。它在Web应用程序的特权下在用户的浏览器中运行。
要防御这种攻击,请确保始终过滤/清理用户输入。
SQL注入
SQL注入包括通过用户输入注入部分或完整的SQL查询。它既可以读取敏感信息,也可以具有破坏性。
select title, author from books where id=$id
在这个例子$id来自用户 - 如果用户进入该2 or 1=1怎么办?查询变为以下内容:
select title, author from books where id=2 or 1=1
防御这类攻击的最简单方法是使用参数化查询或预处理语句。
如果用的是PostgreSQL,那么可以使用node-postgres模块。
创建参数化查询:
var q = 'SELECT name FROM books WHERE id = $1';
client.query(q, ['3'], function(err, result) {});
sqlmap是一个开源的渗透测试工具,可以自动检测和利用SQL注入漏洞并接管数据库服务器。使用此工具测试应用程序的SQL注入漏洞。
命令注入
命令注入是攻击者用于在远程Web服务器上运行OS命令的技术。使用这种方法,攻击者甚至可能会获得系统密码。
在实践中,如果你有一个像这样的URL:
https://example.com/downloads?file=user1.txt
它可以变成:
https://example.com/downloads?file=%3Bcat%20/etc/passwd
在此示例中,%3B
将成为分号,因此可以运行多个OS命令。
要防御这种攻击,请确保始终过滤/清理用户输入。
另外在nodejs中
child_process.exec('ls', function (err, data) {
console.log(data);
});
在hood下child_process.exec
调用执行/bin/sh
,因此它是一个bash解释器而不是程序启动器。
当用户输入传递给此方法时,这是有问题的 - 可以是反引号$()
,也可以是攻击者注入的新命令。
要克服这个问题,只需使用child_process.execFile
。
安全传输
SSL版本,算法,密钥长度
由于HTTP是明文协议,因此必须通过SSL/TLS
隧道(称为HTTPS)进行保护。如今通常使用高级密码,服务器中的错误配置可用于强制使用弱密码 - 或者最坏的情况下不加密。
使用nmap检查证书信息
nmap --script ssl-cert,ssl-enum-ciphers -p 443,465,993,995 www.example.com
使用sslyze测试SSL/TLS
漏洞
./sslyze.py --regular example.com:443
HSTS
在配置管理部分中,我们简要地触及了这一点 - Strict-Transport-Security
标头强制实施与服务器的安全(HTTP over SSL / TLS)连接。从Twitter获取以下示例:
strict-transport-security:max-age=631138519
这里max-age定义了浏览器应自动将所有HTTP请求转换为HTTPS的秒数。
测试非常简单:
curl -s -D- https://twitter.com/ | grep -i Strict
拒绝服务
帐户锁定
帐户锁定是一种减轻暴力猜测攻击的技术。在实践中,这意味着在少量不成功的登录尝试之后,系统禁止在给定时间段内进行登录尝试(最初可能是几分钟,然后它可以指数增加)。
可以使用之前触及的速率限制器模式来保护的应用程序免受这类攻击。
正则表达式
这种攻击利用了这样一个事实,即大多数正则表达式实现可能会达到极端情况,导致它们工作得非常慢。这些正则表达式被称为邪恶的正则表达式:
- 重复分组
- 在重复组内
- 重复
- 交替重叠
([a-zA-Z]+)*
,(a+)+
或者(a|a?)+
都是易受攻击的正则表达式作为简单的输入,aaaaaaaaaaaaaaaaaaaaaaaa!
可能导致繁重的计算。
要检查Regex对这些,可以使用名为safe-regex的Node.js工具。它可能会产生误报,因此请谨慎使用。
$ node safe.js '(beep|boop)*'
true
$ node safe.js '(a+){10}'
false
错误处理
错误代码,堆栈跟踪
在不同的错误情况的应用程序可能会泄漏对底层基础架构的敏感细节,如:X-Powered-By:Express
。
堆栈跟踪本身不被视为漏洞,但它们通常会泄露攻击者可能感兴趣的信息。由于产生错误的操作而提供调试信息被认为是不好的做法。应该始终记录它们,但不要向用户显示它们。
NPM
强大的功能带来了很大的责任 - NPM有很多可以立即使用的软件包,但需要付出代价:应该检查应用程序需要什么。它们可能包含至关重要的安全问题。
node安全项目
幸运的是,Node Security project有一个很棒的工具nsp,可以检查使用过的模块是否存在已知漏洞。
npm i nsp -g
# either audit the shrinkwrap
nsp audit-shrinkwrap
# or the package.json
nsp audit-package
Snyk
Snyk类似于Node Security Project,但其目的是提供一种工具,不仅可以检测,还可以修复代码库中与安全相关的问题。
HTTP Basic authentication
HTTP基本身份验证是客户端在发出请求时提供用户名和密码的方法。
这是实施访问控制的最简单方法,因为它不需要cookie,会话或其他任何东西。要使用它,客户端必须发送Authorization标头以及它所做的每个请求。用户名和密码未加密,但以这种方式构造:
- 用户名和密码连接成一个字符串:
username:password
- 此字符串使用Base64编码
- 该
Basic
关键字此编码值前放
john:secret
base64编码成am9objpzZWNyZXQ=
curl --header "Authorization: Basic am9objpzZWNyZXQ=" my-website.com
import basicAuth from 'basic-auth';
function unauthorized(res) {
res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
return res.send(401);
};
export default function auth(req, res, next) {
const {name, pass} = basicAuth(req) || {};
if (!name || !pass) {
return unauthorized(res);
};
if (name === 'john' && pass === 'secret') {
return next();
}
return unauthorized(res);
};
也可以通过nginx配置
缺点:
- 用户名和密码随每个请求一起发送,可能会暴露它们 - 即使是通过安全连接发送的
- 连接到SSL / TLS,如果网站使用弱加密,或者攻击者可以破解它,则会立即暴露用户名和密码
- 没有办法使用Basic auth注销用户
- 凭证到期并非易事 - 您必须要求用户更改密码才能这样做
Cookies
当服务器在响应中收到HTTP请求时,它可以发送Set-Cookie
标头。浏览器将其放入cookie jar中,并且cookie将与每个对CookieHTTP头中相同源的请求一起发送。
要使用cookie进行身份验证,必须遵循一些关键原则。
始终使用HttpOnly cookie
为了减轻XSS攻击的可能性,HttpOnly在设置cookie时始终使用该标志。这样他们就不会出现了document.cookies。
始终使用签名的cookie
使用签名cookie,服务器可以判断客户端是否修改了cookie。
这也可以在Chrome中观察到 - 首先让我们看一下服务器如何设置cookie:
稍后,所有请求都使用为给定域设置的cookie:
缺点:
- 需要付出额外的努力来缓解CSRF攻击
- 与REST不兼容 - 因为它将状态引入无状态协议
令牌(Tokens)
如今JWT(JSON Web Token)无处不在 - 仍值得研究潜在的安全问题。
JWT由三部分组成:
- 标头(Header),包含令牌类型和散列算法
- 有效负载(Payload),包含 claims
- 签名(Signature),如果选择HMAC SHA256,可以按如下方式计算:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
var koa = require('koa');
var jwt = require('koa-jwt');
var app = koa();
app.use(jwt({
secret: 'very-secret'
}));
// Protected middleware
app.use(function *(){
// content of the token will be available on this.state.user
this.body = {
secret: '42'
};
});
curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com
与之前的一样,也可以在Chrome中观察到令牌:
在浏览器中使用JWT,必须将其存储在LocalStorage或SessionStorage中,这可能导致XSS攻击。
缺点: 需要额外努力来缓解XSS攻击
签名(Signatures)
使用cookies或tokens,如果传输层因任何原因被暴露,你的凭据(credentials)很容易访问 - 并且使用tokens或cookie,攻击者可以像真实用户一样行事。
解决这个问题的一种可能方法 - 在讨论API时而不是浏览器时,请签署每个请求。
当API的消费者发出请求时,它必须对其进行签名,这意味着它必须使用私钥从整个请求创建散列。
对于该哈希计算,可以使用:
- HTTP方法
- 请求的路径
- HTTP标头
- HTTP有效负载的校验和(Checksum of the HTTP payload)
- 和一个私钥来创建哈希(and a private key to create the hash)
为了使其工作,API的使用者和提供者都必须具有相同的私钥。获得签名后,您必须将其添加到请求中,无论是查询字符串还是HTTP标头。此外,还应添加日期,以便可以定义到期日期。
为什么要经历所有这些步骤?因为即使传输层受到攻击,攻击者也只能读取你的流量,无法充当用户,因为攻击者无法签署请求 - 因为私钥不在ta的拥有中。大多数AWS服务都使用这种身份验证。
可以尝试使用node-http-signature处理HTTP请求签名。
缺点:不能在浏览器/客户端中使用,只能在API之间使用
一次性密码
一次性密码算法生成一次性密码,其中包含共享密钥以及当前时间或计数器:
- 基于时间的一次性密码算法,基于当前时间。
- 基于HMAC的一次性密码算法,基于计数器。
这些方法用于利用双因素身份验证的应用程序:用户输入用户名和密码,然后服务器和客户端都生成一次性密码。
在Node.js中,使用notp实现它相对容易。
缺点:
- 使用共享密钥(如果被盗)可以模拟用户令牌
- 因为客户端可能被窃取/出错每个实时应用程序都有方法绕过这个,比如电子邮件重置会为应用程序添加额外的攻击向量
该在何时选择何种验证方法?
如果只需支持一个web应用,那么cookies和tokens的实现都是可以的(cookies对XSRF的防护较好,而JWT则更易于防护XSS)。
如果需要同时支持web应用和移动客户端,那么请使用基于token的验证。
如果正在构建仅与其他API通信的API,那么就使用signatures。
SSL
Secure Sockets Layer,这是其全名,他的作用是协议,定义了用来对网络发出的数据进行加密的格式和规则。
+------+ +------+
服务器 | data | -- SSL 加密 --> 发送 --> 接收 -- SSL 解密 -- | data | 客户端
+------+ +------+
+------+ +------+
服务器 | data | -- SSL 解密 --> 接收 <-- 发送 -- SSL 加密 -- | data | 客户端
+------+ +------+
协议
所谓协议,是程序员在编程时,大家对发送的数据格式采用的一种共同结构,也就是标准数据结构。
比如, HTTP 请求头的协议采用
GET /demo HTTP/1.1\n
Host: www.google.com\n
Connection: keep-alive\n
这样的格式,每一个厂商在开发的网络软件都遵循这一标准。这样在另一个接受端,可以采用相同的解析代码来编程。
OpenSSL
OpenSSL 是在程序上对 SSL 标准的一个实现,提供了:
- libcrypto 通用加密库
- libssl TLS/SSL 的实现
- openssl 命令行工具
程序员可以通过免费开源的 OpenSSL 库来对自己的应用程序提供 SSL 加密。OpenSSL 由 Eric A. Young 和 Tim J. Hudson 在 1995 年提出。1998年,OpenSSL 项目组接管,并制定标准规范。
SSH
telnet是一个不安全的通过命令行进行服务器客户端通信的工具。
SSH 是使用 OpenSSL 加密的这样的通信工具,提供了更多安全功能。
服务器安装 $ sudo aptitude install openssh-server
服务器启动 $ sudo service ssh start
客户端连接 $ ssh qm@192.168.1.101
Node.js 是完全采用 OpenSSL 进行加密的,其相关的 TLS HTTPS 服务器模块和 Crypto 加密模块都是通过 C++ 在底层调用 OpenSSL 。
OpenSSL --> crypto --> tls --> https
OpenSSL 实现了对称加密:
AES(128) | DES(64) | Blowfish(64) | CAST(64) | IDEA(64) | RC2(64) | RC5(64)
非对称加密:
DH | RSA | DSA | EC
以及一些信息摘要:
MD2 | MD5 | MDC2 | SHA | RIPEMD | DSS
其中信息摘要是一些采用哈希算法的加密方式,也意味着这种加密是单向的,不能反向解密。这种方式的加密大多是用于保护安全口令,比如登录密码。这里面最长用的是 MD5 和 SHA (建议采用更稳定的 SHA1)。