跳到主要内容

Node.js 安全

问题

Node.js 应用有哪些常见安全问题?如何防范?

答案

Node.js 应用面临输入验证、依赖安全、认证授权、数据保护等多方面安全挑战,需要从代码、配置、依赖等多个层面防护。


常见安全漏洞

1. SQL 注入

// ❌ 危险:字符串拼接
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// 攻击: userId = "1' OR '1'='1"

// ✅ 安全:参数化查询
import { Pool } from 'pg';

const pool = new Pool();

async function getUser(userId: string) {
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
return result.rows[0];
}

// ✅ 使用 ORM
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const user = await prisma.user.findUnique({
where: { id: userId }
});

2. NoSQL 注入

// ❌ 危险:直接使用用户输入
const user = await db.collection('users').findOne({
username: req.body.username,
password: req.body.password
});
// 攻击: { "password": { "$ne": "" } }

// ✅ 安全:验证输入类型
import { z } from 'zod';

const loginSchema = z.object({
username: z.string().min(1).max(50),
password: z.string().min(6).max(100)
});

const { username, password } = loginSchema.parse(req.body);
const hashedPassword = await bcrypt.hash(password, 10);

3. XSS(跨站脚本)

// ❌ 危险:直接输出用户输入
app.get('/search', (req, res) => {
res.send(`<h1>搜索: ${req.query.q}</h1>`);
});
// 攻击: ?q=<script>alert('xss')</script>

// ✅ 安全:转义输出
import { escape } from 'html-escaper';

app.get('/search', (req, res) => {
res.send(`<h1>搜索: ${escape(req.query.q as string)}</h1>`);
});

// ✅ 使用模板引擎自动转义
// EJS, Pug 等默认转义

4. 命令注入

// ❌ 危险:拼接命令
import { exec } from 'child_process';

app.get('/ping', (req, res) => {
exec(`ping -c 1 ${req.query.host}`, (err, stdout) => {
res.send(stdout);
});
});
// 攻击: ?host=127.0.0.1; rm -rf /

// ✅ 安全:使用 execFile + 参数数组
import { execFile } from 'child_process';

app.get('/ping', (req, res) => {
const host = req.query.host as string;

// 验证输入
if (!/^[\w.-]+$/.test(host)) {
return res.status(400).send('Invalid host');
}

execFile('ping', ['-c', '1', host], (err, stdout) => {
res.send(stdout);
});
});

5. 路径遍历

// ❌ 危险:直接拼接路径
import path from 'path';
import fs from 'fs';

app.get('/files/:name', (req, res) => {
const filePath = `./uploads/${req.params.name}`;
res.sendFile(filePath);
});
// 攻击: /files/../../../etc/passwd

// ✅ 安全:验证路径
app.get('/files/:name', (req, res) => {
const basePath = path.resolve('./uploads');
const filePath = path.resolve(basePath, req.params.name);

// 确保在允许的目录内
if (!filePath.startsWith(basePath)) {
return res.status(403).send('Access denied');
}

res.sendFile(filePath);
});

依赖安全

检查依赖漏洞

# npm 内置审计
npm audit
npm audit fix

# 使用 Snyk
npx snyk test
npx snyk monitor

# 使用 OWASP dependency-check

package.json 安全配置

{
"scripts": {
"audit": "npm audit --audit-level=high",
"preinstall": "npx npm-force-resolutions"
},
"overrides": {
"vulnerable-package": "^2.0.0"
}
}

认证与授权

密码安全

import bcrypt from 'bcrypt';

// 加密密码
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}

// 验证密码
async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}

JWT 安全

import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;

interface TokenPayload {
userId: string;
role: string;
}

// 短期 Access Token
function generateAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, SECRET, { expiresIn: '15m' });
}

// 长期 Refresh Token
function generateRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' });
}

// 验证
function verifyToken(token: string): TokenPayload {
return jwt.verify(token, SECRET) as TokenPayload;
}

安全 HTTP 头

import helmet from 'helmet';
import express from 'express';

const app = express();

// Helmet 自动设置安全头
app.use(helmet());

// 或手动配置
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true
}
}));

// 手动设置
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});

速率限制

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

const redisClient = createClient();

// 基础限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每 IP 100 次请求
message: 'Too many requests',
standardHeaders: true,
legacyHeaders: false
});

// Redis 存储(适用于多实例)
const distributedLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
store: new RedisStore({
sendCommand: (...args: string[]) => redisClient.sendCommand(args)
})
});

// 登录接口严格限制
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 小时
max: 5, // 最多 5 次失败
skipSuccessfulRequests: true
});

app.use('/api/', limiter);
app.post('/login', loginLimiter, loginHandler);

常见面试问题

Q1: 如何防止 SQL/NoSQL 注入?

答案

方法SQLNoSQL
参数化查询使用占位符 $1, ?使用 ORM/ODM
输入验证类型、长度、格式检查Zod/Joi 验证
ORMPrisma, TypeORMMongoose
最小权限数据库用户权限限制角色限制
// Prisma 自动防注入
const users = await prisma.user.findMany({
where: {
name: { contains: userInput } // 自动转义
}
});

// Zod 输入验证
const schema = z.object({
id: z.string().uuid(),
name: z.string().max(100).regex(/^[\w\s]+$/)
});

Q2: Node.js 应用如何防止 DDoS 攻击?

答案

// 1. 速率限制
import rateLimit from 'express-rate-limit';

// 2. 请求体大小限制
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ limit: '10kb', extended: true }));

// 3. 超时设置
const server = app.listen(3000);
server.setTimeout(30000); // 30 秒超时

// 4. 负载均衡 + CDN

// 5. 使用 slowloris 防护
import slowDown from 'express-slow-down';

const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 100,
delayMs: (hits) => hits * 100
});

Q3: 如何安全存储敏感配置?

答案

// 1. 环境变量
const dbPassword = process.env.DB_PASSWORD;

// 2. .env 文件(不提交到 Git)
import dotenv from 'dotenv';
dotenv.config();

// 3. 密钥管理服务
// AWS Secrets Manager, HashiCorp Vault

// 4. 验证必需配置
const requiredEnvVars = ['DB_HOST', 'DB_PASSWORD', 'JWT_SECRET'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required env var: ${envVar}`);
}
}

// 5. 避免在日志中泄露
const sanitizeLog = (obj: any) => {
const sensitive = ['password', 'token', 'secret', 'key'];
return JSON.stringify(obj, (key, value) =>
sensitive.some(s => key.toLowerCase().includes(s))
? '[REDACTED]'
: value
);
};

Q4: JWT 有哪些安全注意事项?

答案

注意点说明
算法使用 RS256 或 HS256,禁用 none
过期时间Access Token 短期(15分钟)
存储HttpOnly Cookie 或内存
刷新使用 Refresh Token 轮换
作废维护黑名单或版本号
// 安全配置
const token = jwt.sign(
{ userId, version: user.tokenVersion },
SECRET,
{
algorithm: 'HS256',
expiresIn: '15m',
issuer: 'my-app',
audience: 'my-app-users'
}
);

// 验证时检查版本
function verifyToken(token: string, user: User) {
const payload = jwt.verify(token, SECRET);
if (payload.version !== user.tokenVersion) {
throw new Error('Token revoked');
}
return payload;
}

Q5: 如何进行安全审计?

答案

# 1. 依赖审计
npm audit
npx snyk test

# 2. 代码扫描
npx eslint-plugin-security
npx njsscan .

# 3. SAST 工具
# SonarQube, Semgrep
// 安全检查清单
const securityChecklist = {
dependencies: '定期运行 npm audit',
headers: '使用 helmet 设置安全头',
input: '所有输入使用 Zod/Joi 验证',
sql: '使用参数化查询或 ORM',
auth: '密码 bcrypt,JWT 短期过期',
secrets: '环境变量,不硬编码',
https: '生产环境强制 HTTPS',
logging: '不记录敏感信息',
rateLimit: 'API 速率限制',
cors: '严格配置允许的源'
};

相关链接