security-and-hardening

security-and-hardening

热门

强化代码以抵御漏洞。适用于处理用户输入、身份验证、数据存储或外部集成时。适用于构建任何接受不可信数据、管理用户会话或与第三方服务交互的功能。

7.7万Star
0Fork
更新于 2026/7/3
SKILL.md
readonly只读
name
security-and-hardening
description

强化代码以抵御漏洞。适用于处理用户输入、身份验证、数据存储或外部集成时。适用于构建任何接受不可信数据、管理用户会话或与第三方服务交互的功能。

安全与加固

概述

Web 应用的安全优先开发实践。将每个外部输入视为恶意,每个秘密视为神圣,每个授权检查视为强制。安全不是一个阶段——它是每一行涉及用户数据、身份验证或外部系统的代码的约束。

何时使用

  • 构建任何接受用户输入的功能
  • 实现身份验证或授权
  • 存储或传输敏感数据
  • 与外部 API 或服务集成
  • 添加文件上传、Webhook 或回调
  • 处理支付或 PII 数据

流程:先进行威胁建模

没有威胁模型就添加控制措施只是猜测。在加固之前,花五分钟像攻击者一样思考:

  1. 映射信任边界。 不可信数据在哪里进入你的系统?HTTP 请求、表单字段、文件上传、Webhook、第三方 API、消息队列以及 LLM 输出。每个边界都是攻击面。
  2. 命名资产。 什么值得窃取或破坏?凭据、PII、支付数据、管理员操作、资金转移。
  3. 对每个边界运行 STRIDE——快速审视,而非仪式:
威胁 问题 典型缓解措施
Spoofing(欺骗) 能否有人冒充用户/服务? 身份验证、签名验证
Tampering(篡改) 数据在传输或存储中能否被篡改? 完整性检查、参数化查询、HTTPS
Repudiation(抵赖) 能否事后否认某个操作? 安全事件审计日志
Information disclosure(信息泄露) 数据是否会泄露? 加密、字段白名单、通用错误信息
Denial of service(拒绝服务) 系统能否被压垮? 速率限制、输入大小上限、超时
Elevation of privilege(权限提升) 用户能否获得不应有的权限? 授权检查、最小权限原则
  1. 在用例旁边编写滥用案例。 对于每个功能,问自己“我该如何滥用它?”——然后将其作为你的第一个测试。

如果你无法命名某个功能的信任边界,你就还没有准备好保护它。这是 OWASP A04: 不安全设计——大多数漏洞始于设计,而非代码。

三层边界系统

始终执行(无例外)

  • 在系统边界验证所有外部输入(API 路由、表单处理器)
  • 参数化所有数据库查询——永远不要将用户输入拼接到 SQL 中
  • 编码输出以防止 XSS(使用框架自动转义,不要绕过它)
  • 对所有外部通信使用 HTTPS
  • 使用 bcrypt/scrypt/argon2 哈希密码(绝不存储明文)
  • 设置安全头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
  • 对会话使用 httpOnly、secure、sameSite cookie
  • 在每次发布前运行 npm audit(或等效工具)

先询问(需要人工批准)

  • 添加新的身份验证流程或更改身份验证逻辑
  • 存储新类别的敏感数据(PII、支付信息)
  • 添加新的外部服务集成
  • 更改 CORS 配置
  • 添加文件上传处理器
  • 修改速率限制或节流
  • 授予提升的权限或角色

绝不执行

  • 绝不将秘密提交到版本控制(API 密钥、密码、令牌)
  • 绝不记录敏感数据(密码、令牌、完整信用卡号)
  • 绝不信任客户端验证作为安全边界
  • 绝不为了方便而禁用安全头
  • 绝不使用 eval()innerHTML 处理用户提供的数据
  • 绝不将会话存储在客户端可访问的存储中(localStorage 中的认证令牌)
  • 绝不向用户暴露堆栈跟踪或内部错误详情

OWASP Top 10 预防模式

以下是预防模式,并非排名。有关 2021 年顺序,请参阅 references/security-checklist.md 中的快速参考表。

注入(SQL、NoSQL、OS 命令)

// 错误:通过字符串拼接导致的 SQL 注入
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// 正确:参数化查询
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

// 正确:使用参数化输入的 ORM
const user = await prisma.user.findUnique({ where: { id: userId } });

身份验证失效

// 密码哈希
import { hash, compare } from 'bcrypt';

const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);

// 会话管理
app.use(session({
  secret: process.env.SESSION_SECRET,  // 来自环境变量,而非代码
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // 不可通过 JavaScript 访问
    secure: true,       // 仅 HTTPS
    sameSite: 'lax',    // CSRF 保护
    maxAge: 24 * 60 * 60 * 1000,  // 24 小时
  },
}));

跨站脚本攻击(XSS)

// 错误:将用户输入渲染为 HTML
element.innerHTML = userInput;

// 正确:使用框架自动转义(React 默认如此)
return <div>{userInput}</div>;

// 如果必须渲染 HTML,先进行清理
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);

访问控制失效

// 始终检查授权,而不仅仅是身份验证
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
  const task = await taskService.findById(req.params.id);

  // 检查认证用户是否拥有此资源
  if (task.ownerId !== req.user.id) {
    return res.status(403).json({
      error: { code: 'FORBIDDEN', message: '未授权修改此任务' }
    });
  }

  // 继续更新
  const updated = await taskService.update(req.params.id, req.body);
  return res.json(updated);
});

安全配置错误

// 安全头(Express 使用 helmet)
import helmet from 'helmet';
app.use(helmet());

// 内容安全策略
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],  // 尽可能收紧
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'"],
  },
}));

// CORS — 限制为已知来源
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));

敏感数据泄露

// 绝不在 API 响应中返回敏感字段
function sanitizeUser(user: UserRecord): PublicUser {
  const { passwordHash, resetToken, ...publicFields } = user;
  return publicFields;
}

// 使用环境变量存储秘密
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY 未配置');

服务器端请求伪造(SSRF)

每当服务器获取用户影响的 URL 时——Webhook、“从 URL 导入”、图片代理、链接预览——攻击者可以将其指向内部服务(云元数据、localhost、私有 IP)。

// 错误:直接获取用户提供的任何内容
await fetch(req.body.webhookUrl);

// 正确:白名单方案 + 主机,如果任何解析的 IP 是私有的则拒绝,禁止重定向
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';

const ALLOWED_HOSTS = new Set(['hooks.example.com']);

async function assertSafeUrl(raw: string): Promise<URL> {
  const url = new URL(raw);
  if (url.protocol !== 'https:') throw new Error('仅支持 https');
  if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('主机不允许');
  // 解析所有记录;单个私有/保留地址即导致检查失败。
  const addrs = await lookup(url.hostname, { all: true });
  if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
    throw new Error('私有/保留 IP');
  }
  return url;
}

await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });

range() !== 'unicast' 检查覆盖了回环、链路本地 169.254.169.254(云元数据,SSRF 头号目标)、私有和唯一本地地址范围(IPv4 和 IPv6)。

注意——这仍然存在 TOCTOU 间隙。 fetch 在检查后再次解析 DNS,因此使用短 TTL 记录的攻击者可以在验证和连接之间将域名重新绑定到内部 IP。对于高风险面,解析一次并连接到固定的 IP,或者在前端放置过滤代理(request-filtering-agent / ssrf-req-filter)。

输入验证模式

边界处的模式验证

import { z } from 'zod';

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200).trim(),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional(),
});

// 在路由处理器中验证
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: '无效输入',
        details: result.error.flatten(),
      },
    });
  }
  // result.data 现在具有类型且已验证
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

文件上传安全

// 限制文件类型和大小
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB

function validateUpload(file: UploadedFile) {
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
    throw new ValidationError('不允许的文件类型');
  }
  if (file.size > MAX_SIZE) {
    throw new ValidationError('文件过大(最大 5MB)');
  }
  // 不要信任文件扩展名——如果关键,检查魔数
}

处理 npm audit 结果

并非所有审计发现都需要立即处理。使用此决策树:

npm audit 报告漏洞
├── 严重性:严重或高
│   ├── 易受攻击的代码在你的应用中是否可达?
│   │   ├── 是 --> 立即修复(更新、修补或替换依赖)
│   │   └── 否(仅开发依赖、未使用的代码路径)--> 尽快修复,但不阻塞
│   └── 是否有可用修复?
│       ├── 是 --> 更新到已修补版本
│       └── 否 --> 检查变通方案,考虑替换依赖,或添加到白名单并设置审查日期
├── 严重性:中等
│   ├── 生产环境中可达?--> 在下一个发布周期修复
│   └── 仅开发?--> 方便时修复,在待办事项中跟踪
└── 严重性:低
    └── 在常规依赖更新期间跟踪并修复

关键问题:

  • 易受攻击的函数是否实际在你的代码路径中被调用?
  • 依赖是运行时依赖还是仅开发依赖?
  • 根据你的部署上下文,漏洞是否可利用(例如,仅客户端应用中的服务器端漏洞)?

当你推迟修复时,记录原因并设置审查日期。

供应链卫生

npm audit 捕获已知的 CVE;它不会捕获恶意或拼写错误的包。此外:

  • 提交 lockfile 并在 CI 中使用 npm ci(而非 npm install)安装——可重现构建,无静默版本漂移。
  • 在添加新依赖之前进行审查——维护情况、下载量以及它们是否真正值得加入。每个依赖都是攻击面(OWASP A06: 易受攻击的组件LLM03: 供应链)。
  • 警惕不熟悉包中的 postinstall 脚本——它们在安装时运行任意代码。
  • 注意拼写错误——cross-env vs crossenvreact-dom vs reactdom

速率限制

import rateLimit from 'express-rate-limit';

// 通用 API 速率限制
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100,                   // 每个窗口 100 个请求
  standardHeaders: true,
  legacyHeaders: false,
}));

// 认证端点更严格的限制
app.use('/api/auth/', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,  // 每 15 分钟 10 次尝试
}));

秘密管理

.env 文件:
  ├── .env.example  → 提交(包含占位值的模板)
  ├── .env          → 不提交(包含真实秘密)
  └── .env.local    → 不提交(本地覆盖)

.gitignore 必须包含:
  .env
  .env.local
  .env.*.local
  *.pem
  *.key

提交前始终检查:

# 检查意外暂存的秘密
git diff --cached | grep -i "password\|secret\|api_key\|token"

如果秘密曾经被提交,立即轮换它。 删除行或重写历史是不够的——一旦到达远程,就假设它已被泄露。首先撤销并重新颁发密钥,然后从历史中清除。

保护 AI / LLM 功能

如果你的应用调用 LLM——聊天机器人、摘要器、代理、RAG——它会继承一个新的攻击面。将其映射到 OWASP Top 10 for LLM Applications (2025)

  • 将所有模型输出视为不可信输入(LLM05: 输出处理不当)。 切勿将 LLM 输出直接传入 eval、SQL、shell、innerHTML 或文件路径。像处理原始用户输入一样验证和编码。
  • 假设提示可能被劫持(LLM01: 提示注入)。 上下文窗口中的不可信文本——用户消息、获取的网页、PDF——可能携带指令。系统提示不是安全边界;在代码中强制执行权限,而非在提示中。
  • 将秘密和其他用户的数据排除在提示之外(LLM02 / LLM07)。 上下文中的任何内容都可能被回显。不要将 API 密钥、跨租户数据或完整系统提示放在模型可能重复的位置。
  • 限制工具和代理权限(LLM06: 过度代理)。 将工具范围限定为最小,对破坏性或不可逆操作要求确认,并验证每个工具参数。
  • 限制消耗(LLM10: 无限制消耗)。 限制令牌、请求速率以及循环/递归深度,以防止精心构造的输入导致成本飙升或系统挂起。
  • 隔离检索数据(LLM08: 向量和嵌入弱点)。 在 RAG 中,将向量存储视为信任边界:按租户分区嵌入,以便一个用户无法检索另一个用户的数据,并在索引前验证文档,以防止有毒内容引导答案。
// 错误:信任模型输出作为命令或标记
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
await db.query(sql);                                   // 任意查询执行
container.innerHTML = await llm.reply(userMessage);   // 通过模型的存储型 XSS

// 正确:模型输出是数据——防御性解析,然后验证,然后编码
let intent;
try {
  intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
} catch {
  throw new ValidationError('意外的模型输出'); // JSON.parse 或模式失败
}
await runAllowlistedAction(intent.action, intent.params);
container.textContent = await llm.reply(userMessage);

安全审查清单

### 身份验证
- [ ] 密码使用 bcrypt/scrypt/argon2 哈希(盐轮数 ≥ 12)
- [ ] 会话令牌为 httpOnly、secure、sameSite
- [ ] 登录有速率限制
- [ ] 密码重置令牌会过期

### 授权
- [ ] 每个端点检查用户权限
- [ ] 用户只能访问自己的资源
- [ ] 管理员操作需要管理员角色验证

### 输入
- [ ] 所有用户输入在边界处验证
- [ ] SQL 查询已参数化
- [ ] HTML 输出已编码/转义
- [ ] 服务器端 URL 获取已白名单化(无 SSRF 到内部服务)

### 数据
- [ ] 代码或版本控制中没有秘密
- [ ] 敏感字段已从 API 响应中排除
- [ ] PII 在静态时加密(如适用)

### 基础设施
- [ ] 安全头已配置(CSP、HSTS 等)
- [ ] CORS 限制为已知来源
- [ ] 依赖项已审计漏洞
- [ ] 错误消息不暴露内部信息

### 供应链
- [ ] Lockfile 已提交;CI 使用 `npm ci` 安装
- [ ] 新依赖已审查(维护、下载量、postinstall 脚本)

### AI / LLM(如果使用)
- [ ] 模型输出视为不可信(无 eval/SQL/innerHTML/shell)
- [ ] 秘密和其他用户的数据排除在提示之外
- [ ] 工具/代理权限已限定;破坏性操作需要确认

另请参阅

有关详细的安全清单和预提交验证步骤,请参阅 references/security-checklist.md

常见合理化借口

合理化借口 现实
“这是内部工具,安全不重要” 内部工具也会被攻破。攻击者瞄准最薄弱的环节。
“我们以后再加安全” 安全改造比一开始就构建难 10 倍。现在就加上。
“没人会试图利用这个” 自动化扫描器会发现它。安全通过模糊不是安全。
“框架处理安全” 框架提供工具,而非保证。你仍然需要正确使用它们。
“这只是原型” 原型会变成生产环境。从第一天起就养成安全习惯。
“威胁建模在这里是过度设计” 五分钟的“我该如何攻击它?”可以防止后续任何控制措施都无法修补的设计缺陷。
“这只是 LLM 输出,只是文本” 那个“文本”可以是 SQL 语句、脚本标签或 shell 命令。像对待任何不可信输入一样对待它。

危险信号

  • 用户输入直接传递给数据库查询、shell 命令或 HTML 渲染
  • 源代码或提交历史中存在秘密
  • API 端点没有身份验证或授权检查
  • 缺少 CORS 配置或通配符(*)来源
  • 认证端点没有速率限制
  • 向用户暴露堆栈跟踪或内部错误
  • 依赖项存在已知的严重漏洞
  • 服务器获取用户提供的 URL 而没有白名单(SSRF)
  • LLM/模型输出传入查询、DOM、shell 或 eval
  • 秘密、PII 或完整系统提示放入 LLM 上下文窗口

验证

在实现安全相关代码后:

  • [ ] npm audit 显示没有严重或高漏洞
  • [ ] 源代码或 git 历史中没有秘密
  • [ ] 所有用户输入在系统边界处验证
  • [ ] 每个受保护端点检查身份验证和授权
  • [ ] 响应中存在安全头(使用浏览器 DevTools 检查)
  • [ ] 错误响应不暴露内部细节
  • [ ] 认证端点已启用速率限制
  • [ ] 服务器端 URL 获取已根据白名单验证(无 SSRF)
  • [ ] LLM/模型输出在使用前已验证和编码(如果存在 AI 功能)