MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js HTTP 服务的安全防护措施

2023-11-132.0k 阅读

输入验证

在 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 - Typeapplication/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 服务的安全性,保护用户数据和服务的正常运行。在实际开发中,应根据具体的业务需求和安全威胁场景,灵活运用这些措施,并持续关注安全动态,及时更新防护策略。