Node.js HTTP 服务的安全防护措施
输入验证
在 Node.js 的 HTTP 服务开发中,输入验证是安全防护的第一道防线。用户通过 HTTP 请求发送的数据可能包含恶意内容,如 SQL 注入、跨站脚本攻击(XSS)等攻击向量。因此,对输入数据进行严格验证十分关键。
验证请求参数
以使用 Express 框架为例,假设我们有一个接收用户登录信息的接口,需要验证用户名和密码的格式。
const express = require('express');
const app = express();
const Joi = require('joi');
// 定义验证规则
const loginSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required()
});
app.post('/login', (req, res) => {
const { error } = loginSchema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
// 正常处理登录逻辑
res.send('Login successful');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在上述代码中,我们使用 Joi 库来定义用户名和密码的验证规则。用户名必须是字母和数字组成,长度在 3 到 30 个字符之间,且为必填项;密码必须由字母和数字组成,长度在 3 到 30 个字符之间,也是必填项。通过 validate
方法对请求体进行验证,如果验证失败,返回 400 错误及具体错误信息。
验证请求头
请求头也可能被恶意篡改,比如 User - Agent
头可能被伪造来绕过一些基于用户代理的限制。我们可以验证一些关键的请求头字段。
app.get('/', (req, res) => {
const userAgent = req.get('User - Agent');
if (!userAgent.match(/(Chrome|Firefox|Safari)/)) {
return res.status(403).send('Only Chrome, Firefox and Safari are allowed');
}
res.send('Welcome');
});
这段代码验证了 User - Agent
头是否包含 Chrome、Firefox 或 Safari,若不包含则返回 403 禁止访问错误。
防止 SQL 注入
如果 Node.js 的 HTTP 服务与数据库交互,SQL 注入是一个严重的安全威胁。恶意用户可能通过构造特殊的输入来修改 SQL 查询逻辑,从而获取敏感数据或执行非授权的数据库操作。
使用参数化查询
以 MySQL 数据库为例,使用 mysql
模块。
const mysql = require('mysql');
const express = require('express');
const app = express();
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test'
});
connection.connect();
app.get('/user/:id', (req, res) => {
const id = req.params.id;
const query = 'SELECT * FROM users WHERE id =?';
connection.query(query, [id], (error, results, fields) => {
if (error) throw error;
res.send(results);
});
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在上述代码中,SELECT * FROM users WHERE id =?
是参数化查询,?
是占位符,实际值通过数组 [id]
传递。这样即使 id
参数被恶意构造,也不会改变 SQL 查询的逻辑,从而防止 SQL 注入。
预编译语句
在 PostgreSQL 中,同样可以使用预编译语句来防止 SQL 注入。
const { Pool } = require('pg');
const express = require('express');
const app = express();
const pool = new Pool({
user: 'user',
host: 'localhost',
database: 'test',
password: 'password',
port: 5432,
});
app.get('/product/:id', (req, res) => {
const id = req.params.id;
const query = 'SELECT * FROM products WHERE id = $1';
pool.query(query, [id], (error, results) => {
if (error) throw error;
res.send(results.rows);
});
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
这里使用 $1
作为占位符,实际值通过数组 [id]
传递,数据库驱动会对输入值进行适当的转义和处理,避免 SQL 注入。
防止 XSS 攻击
跨站脚本攻击(XSS)是指攻击者在网页中注入恶意脚本,当用户访问该网页时,这些脚本会在用户浏览器中执行,从而窃取用户数据或进行其他恶意操作。在 Node.js 的 HTTP 服务开发中,有多种方式来防止 XSS 攻击。
对输出进行转义
在将用户输入的数据输出到 HTML 页面时,必须对其进行转义,以防止脚本被执行。在 Express 应用中,可以使用 ejs - html - encoder
库来转义数据。
const express = require('express');
const app = express();
const ejs = require('ejs');
const encoder = require('ejs - html - encoder');
app.get('/message', (req, res) => {
const message = req.query.message;
const safeMessage = encoder.encode(message);
res.render('message.ejs', { message: safeMessage });
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在 message.ejs
文件中:
<!DOCTYPE html>
<html>
<head>
<title>Message</title>
</head>
<body>
<p><%= message %></p>
</body>
</html>
这样,即使 message
参数包含恶意脚本,也会被转义成普通文本,无法在浏览器中执行。
设置 HTTP 头以防止 XSS
可以设置 Content - Security - Policy
(CSP)头来限制页面可以加载的资源来源,从而防止 XSS 攻击。
app.use((req, res, next) => {
res.setHeader('Content - Security - Policy', "default - src'self'");
next();
});
上述代码设置了 Content - Security - Policy
头,规定页面只能从自身域名加载资源,有效防止了外部恶意脚本的注入。
防止 CSRF 攻击
跨站请求伪造(CSRF)攻击是攻击者诱导用户在已登录的情况下访问恶意链接,从而以用户的身份执行非授权操作。在 Node.js 的 HTTP 服务中,有多种方法来防范 CSRF 攻击。
使用 CSRF 令牌
以 Express 应用为例,使用 csurf
中间件。
const express = require('express');
const csrf = require('csurf');
const app = express();
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form.ejs', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// 处理表单提交逻辑
res.send('Form submitted successfully');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在 form.ejs
文件中:
<!DOCTYPE html>
<html>
<head>
<title>Form</title>
</head>
<body>
<form action="/submit" method="post">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input type="submit" value="Submit">
</form>
</body>
</html>
在上述代码中,csurf
中间件生成 CSRF 令牌,并通过 req.csrfToken()
方法传递给模板。表单提交时,会携带这个令牌,服务器验证令牌的有效性,若令牌无效则拒绝请求,从而防止 CSRF 攻击。
验证 Referer 头
虽然验证 Referer 头不是一种完全可靠的方法,但可以作为辅助手段。
app.post('/transfer', (req, res) => {
const referer = req.get('Referer');
const allowedOrigin = 'http://your - website.com';
if (!referer ||!referer.startsWith(allowedOrigin)) {
return res.status(403).send('Forbidden');
}
// 处理转账逻辑
res.send('Transfer successful');
});
这段代码检查 Referer
头是否来自允许的源,如果不是则返回 403 禁止访问错误。但需要注意的是,Referer
头可能被伪造或由于隐私设置而缺失。
安全的 HTTP 头设置
正确设置 HTTP 头可以增强 Node.js HTTP 服务的安全性,防止多种类型的攻击。
设置 Content - Type 头
确保正确设置 Content - Type
头,防止浏览器错误地解析内容类型,从而导致安全漏洞。
app.get('/data', (req, res) => {
const data = { message: 'Hello' };
res.setHeader('Content - Type', 'application/json');
res.send(data);
});
上述代码明确设置了 Content - Type
为 application/json
,告诉浏览器返回的数据是 JSON 格式,避免浏览器进行错误的解析。
设置 Strict - Transport - Security(HSTS)头
HSTS 头告诉浏览器在一定时间内只能通过 HTTPS 访问网站,防止用户通过 HTTP 访问,从而避免中间人攻击。
app.use((req, res, next) => {
res.setHeader('Strict - Transport - Security', 'max - age = 31536000; includeSubDomains');
next();
});
max - age
设置为一年(31536000 秒),includeSubDomains
表示子域名也适用该规则。
设置 X - Frame - Options 头
X - Frame - Options
头用于防止网站被嵌入到其他网站的框架中,避免点击劫持攻击。
app.use((req, res, next) => {
res.setHeader('X - Frame - Options', 'DENY');
next();
});
DENY
值表示不允许任何网站将当前网站嵌入到框架中。还可以使用 SAMEORIGIN
,表示只允许同域名的网站嵌入。
访问控制与身份验证
合理的访问控制和身份验证机制是保障 Node.js HTTP 服务安全的重要环节。
基于角色的访问控制(RBAC)
在 Express 应用中实现简单的 RBAC。假设我们有管理员和普通用户两种角色,不同角色对 /admin
路由有不同的访问权限。
const express = require('express');
const app = express();
// 模拟用户数据
const users = [
{ id: 1, username: 'admin', role: 'admin' },
{ id: 2, username: 'user', role: 'user' }
];
app.get('/admin', (req, res) => {
const user = users.find(u => u.username === req.query.username);
if (!user) {
return res.status(401).send('Unauthorized');
}
if (user.role!== 'admin') {
return res.status(403).send('Forbidden');
}
res.send('Welcome, admin');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
上述代码通过查询用户名找到对应的用户,然后检查用户角色是否为 admin
,若不是则返回 403 禁止访问错误。
JSON Web Token(JWT)身份验证
使用 JWT 进行身份验证在 Node.js HTTP 服务中很常见。
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
// 模拟用户登录
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'user' && password === 'password') {
const token = jwt.sign({ username }, 'your - secret - key', { expiresIn: '1h' });
res.send({ token });
} else {
res.status(401).send('Unauthorized');
}
});
// 需要身份验证的路由
app.get('/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).send('Unauthorized');
}
try {
const decoded = jwt.verify(token.replace('Bearer ', ''), 'your - secret - key');
res.send(`Welcome, ${decoded.username}`);
} catch (error) {
res.status(401).send('Unauthorized');
}
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
在登录时,生成一个 JWT 并返回给客户端。客户端在后续请求需要身份验证的路由时,将 JWT 放在 Authorization
头中。服务器通过 jwt.verify
方法验证 JWT 的有效性,若有效则允许访问,否则返回 401 未授权错误。
服务器配置安全
除了代码层面的安全防护,服务器的配置安全也至关重要。
保持软件更新
确保 Node.js 版本以及相关依赖包都是最新的。过时的软件版本可能存在已知的安全漏洞。可以使用 npm outdated
命令查看哪些包需要更新,然后使用 npm update
进行更新。
限制服务器暴露的信息
避免在错误信息或响应头中暴露过多的服务器信息。例如,Express 应用默认会在响应头中包含 X - Powered - By: Express
,可以通过自定义响应头来隐藏。
app.use((req, res, next) => {
res.removeHeader('X - Powered - By');
next();
});
这样可以减少攻击者通过服务器信息来寻找攻击切入点的机会。
配置防火墙
在服务器层面配置防火墙,只允许必要的端口和 IP 地址访问。例如,在 Linux 系统中,可以使用 iptables
命令。
# 允许 SSH 连接
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# 允许 HTTP 连接
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# 允许 HTTPS 连接
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# 拒绝其他所有输入连接
iptables -A INPUT -j DROP
上述命令配置了只允许 SSH、HTTP 和 HTTPS 的连接,其他连接都将被拒绝。
安全监控与日志记录
安全监控和日志记录可以帮助及时发现安全问题并进行追溯。
日志记录
在 Express 应用中,使用 morgan
中间件进行日志记录。
const express = require('express');
const app = express();
const morgan = require('morgan');
app.use(morgan('combined'));
app.get('/', (req, res) => {
res.send('Hello');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
morgan('combined')
会记录详细的请求信息,包括请求方法、URL、状态码、响应时间等。这些日志可以帮助分析请求模式,发现异常请求。
安全监控
可以使用工具如 Prometheus 和 Grafana 对 Node.js HTTP 服务进行安全监控。通过自定义指标,如请求频率、错误率等,设置警报规则,当指标超出正常范围时及时通知运维人员。
例如,使用 prom-client
库在 Express 应用中添加自定义指标。
const express = require('express');
const app = express();
const PromClient = require('prom-client');
const appMetrics = new PromClient.Registry();
const httpRequestDurationMicroseconds = new PromClient.Histogram({
name: 'http_request_duration_microseconds',
help: 'Duration of HTTP requests in microseconds',
labelNames: ['method', 'route','status_code'],
buckets: [0.1, 0.5, 1, 5, 10, 50, 100, 500, 1000]
});
appMetrics.registerMetric(httpRequestDurationMicroseconds);
app.use((req, res, next) => {
const start = process.hrtime();
res.on('finish', () => {
const elapsed = process.hrtime(start);
const duration = elapsed[0] * 1e6 + elapsed[1] / 1e3;
httpRequestDurationMicroseconds.labels(req.method, req.path, res.statusCode).observe(duration);
});
next();
});
app.get('/', (req, res) => {
res.send('Hello');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
app.get('/metrics', (req, res) => {
res.set('Content - Type', appMetrics.contentType);
res.end(appMetrics.metrics());
});
});
上述代码定义了一个 http_request_duration_microseconds
指标来记录 HTTP 请求的持续时间。通过 /metrics
路由暴露指标数据,供 Prometheus 采集,然后可以在 Grafana 中进行可视化展示和监控。
通过以上全面的安全防护措施,可以有效提升 Node.js HTTP 服务的安全性,保护用户数据和服务的正常运行。在实际开发中,应根据具体的业务需求和安全威胁场景,灵活运用这些措施,并持续关注安全动态,及时更新防护策略。