johnboe02/javascript-security-checklist
GitHub: johnboe02/javascript-security-checklist
一份基于 OWASP 指南与真实生产事故编写的 JavaScript/TypeScript 应用安全审计与自动化检查清单。
Stars: 0 | Forks: 0
# JavaScript 与 TypeScript 安全清单
一份面向 JavaScript 和 TypeScript 应用(包含 Node.js 后端、React 前端以及 Next.js 全栈应用)的综合安全清单。基于 OWASP 指南、真实生产事故以及自动化代码审查捕获的模式而编写。
你可以将其作为 PR 清单、新人入门参考或审计指南使用。
## 目录
- [输入验证与注入](#1-input-validation--injection)
- [认证与授权](#2-authentication--authorisation)
- [敏感数据泄露](#3-sensitive-data-exposure)
- [XSS(跨站脚本攻击)](#4-xss-cross-site-scripting)
- [CSRF(跨站请求伪造)](#5-csrf-cross-site-request-forgery)
- [依赖安全](#6-dependency-security)
- [错误处理](#7-error-handling)
- [日志与监控](#8-logging--monitoring)
- [Node.js 专项](#9-nodejs-specific)
- [React / 前端专项](#10-react--frontend-specific)
- [TypeScript 安全模式](#11-typescript-security-patterns)
- [支付集成安全](#12-payment-integration-security)
- [如何实现自动化](#how-to-automate-this)
## 1. 输入验证与注入
### SQL 注入
```
// ❌ VULNERABLE — never do this
const results = await db.query(
`SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`
);
// ✅ SAFE — parameterised query
const results = await db.query(
"SELECT * FROM users WHERE email = $1 AND password = $2",
[email, password]
);
// ✅ SAFE — ORM (Prisma)
const user = await prisma.user.findUnique({ where: { email } });
```
**检查清单:**
- [ ] 所有 SQL 查询均使用参数化占位符(`$1`, `?`, `:param`)
- [ ] 查询字符串中不使用字符串拼接或模板字面量
- [ ] ORM 原生查询方法(`prisma.$queryRaw`, `sequelize.query`)使用标签模板字面量或预编译语句
- [ ] 在用于查询前,已对输入长度进行验证
### NoSQL 注入
```
// ❌ VULNERABLE — MongoDB
const user = await User.findOne({ email: req.body.email });
// Attacker sends: { "email": { "$gt": "" } } → returns all users
// ✅ SAFE — sanitise or validate type
import { isEmail } from "validator";
if (!isEmail(req.body.email)) throw new Error("Invalid email");
const user = await User.findOne({ email: req.body.email });
```
**检查清单:**
- [ ] MongoDB 查询需验证输入是否为预期的原始类型
- [ ] `$where` 操作符已被禁用,或严禁与用户输入一起使用
- [ ] 已启用 Mongoose 的 `strict: true`(默认开启,但需验证是否被禁用)
### 命令注入
```
// ❌ VULNERABLE
const { exec } = require("child_process");
exec(`ffmpeg -i ${userFilePath} output.mp4`); // path traversal + injection
// ✅ SAFE — use array form, never shell string
const { execFile } = require("child_process");
execFile("ffmpeg", ["-i", sanitizedPath, "output.mp4"]);
```
**检查清单:**
- [ ] 绝不将用户可控的输入用于 `exec()`
- [ ] 改用 `execFile()` 或 `spawn()` 并以参数数组形式传递
- [ ] 对来自用户输入的文件路径进行验证和清理
- [ ] 阻断路径穿越模式(`../`, `%2e%2e`)
## 2. 认证与授权
### JWT 验证
```
// ❌ VULNERABLE — skips signature verification
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
// ❌ VULNERABLE — algorithm confusion attack
jwt.verify(token, publicKey); // if alg is not specified, attacker can use "none"
// ✅ SAFE
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ["HS256"], // explicitly specify allowed algorithm
issuer: "your-app",
audience: "your-app",
});
```
**检查清单:**
- [ ] 每个经过认证的请求均验证了 JWT 签名
- [ ] `algorithms` 选项明确指定了允许的算法
- [ ] 绝不允许 `none` 算法
- [ ] 已设置并强制校验 Token 过期时间(`exp` 声明)
- [ ] 已实现 Refresh Token 轮换
- [ ] JWT 密钥具有足够长度(>= 256 位)、随机性强,且存储在环境变量中
### 授权检查
```
// ❌ VULNERABLE — horizontal privilege escalation
app.get("/api/orders/:id", authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order); // any authenticated user can read any order
});
// ✅ SAFE — verify ownership
app.get("/api/orders/:id", authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id, // must belong to the requesting user
});
if (!order) return res.status(404).json({ error: "Not found" });
res.json(order);
});
```
**检查清单:**
- [ ] 每个返回用户数据的 endpoint 均通过已认证用户的 ID 进行过滤
- [ ] 管理 endpoint 需验证管理员角色,而不仅仅是身份验证
- [ ] 在执行读/写/删除操作前,已验证资源所有权
- [ ] 角色检查在服务端进行,绝不基于客户端提供的角色声明
- [ ] 所有受保护的路由均已应用身份验证中间件
## 3. 敏感数据泄露
### 代码中的机密信息
```
// ❌ NEVER commit secrets
const STRIPE_SECRET_KEY = "sk_live_abc123...";
const DB_PASSWORD = "mysecretpassword";
// ✅ Always use environment variables
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
if (!STRIPE_SECRET_KEY) throw new Error("STRIPE_SECRET_KEY not set");
```
**检查清单:**
- [ ] 源代码中无 API keys、token、密码或任何机密信息
- [ ] `.env` 文件已添加到 `.gitignore` 中
- [ ] 存在 `.env.example` 文件并仅包含占位符(非真实机密)
- [ ] Git 历史记录干净——使用 `git-secrets` 或 `truffleHog` 进行扫描
- [ ] 生产环境中的机密信息须存储在专业的 secret manager(如 AWS Secrets Manager, Doppler, Vault)中
### API 响应数据过度暴露
```
// ❌ VULNERABLE — exposes internal fields
const user = await User.findById(id);
res.json(user); // includes passwordHash, internalNotes, adminFlags
// ✅ SAFE — explicit field selection or DTO
const user = await User.findById(id).select("name email createdAt");
res.json(user);
// Or with Prisma:
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, name: true, email: true, createdAt: true },
// passwordHash, adminFlags NOT selected
});
```
**检查清单:**
- [ ] API 响应需明确指定返回的字段
- [ ] 响应中绝不能出现 `passwordHash` / `password` 字段
- [ ] 公开响应中需排除内部元数据(`createdBy`, `adminNotes`, `internalId`)
- [ ] 在发送响应前,强制执行序列化层(DTO/schema)的处理
## 4. XSS(跨站脚本攻击)
### React
```
// ❌ VULNERABLE
// ✅ SAFE — React escapes by default
{userContent}
// If HTML rendering is required, sanitise first:
import DOMPurify from "dompurify";
```
**检查清单:**
- [ ] 绝不将未经清理的用户内容用于 `dangerouslySetInnerHTML`
- [ ] 已设置 Content Security Policy (CSP) 标头
- [ ] 严禁将 `eval()` 和 `new Function()` 用于用户输入
- [ ] Markdown 渲染器需对输出进行清理(如使用 DOMPurify 或同等工具)
- [ ] 对来自用户输入的 `href` 值进行验证(拦截 `javascript:` 协议)
```
// ❌ VULNERABLE — href injection
Click here
// ✅ SAFE
const isSafeUrl = (url: string) => /^https?:\/\//.test(url);
Click here
```
## 5. CSRF(跨站请求伪造)
```
// ✅ SAFE — CSRF token with csurf middleware
const csrf = require("csurf");
app.use(csrf({ cookie: { httpOnly: true, secure: true, sameSite: "strict" } }));
app.get("/api/form", (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Alternative: SameSite=Strict cookies (simpler, modern browsers)
res.cookie("session", token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: "strict", // blocks cross-site requests
});
```
**检查清单:**
- [ ] 会更改状态的 endpoint 使用 POST/PUT/PATCH/DELETE 方法(而非 GET)
- [ ] 会话 Cookie 设置为 `SameSite=Strict` 或 `SameSite=Lax`
- [ ] 表单提交需使用 CSRF token
- [ ] 对敏感操作验证 `Origin` 或 `Referer` 标头
## 6. 依赖安全
```
# 审计依赖
npm audit
npm audit --production # production deps only
# 尽可能自动修复
npm audit fix
# 检查过时的 packages
npm outdated
```
**检查清单:**
- [ ] CI 流程中运行 `npm audit`,并在遇到高危/严重漏洞时阻断构建
- [ ] 依赖项定期更新(使用 Dependabot 或 Renovate)
- [ ] 提交了 `package-lock.json` 并锁定依赖版本
- [ ] 不包含存在已知 CVE 漏洞且已停止维护的包
- [ ] 生产环境构建产物中不打包开发依赖项(dev dependencies)
## 7. 错误处理
```
// ❌ VULNERABLE — exposes internals
app.use((err, req, res, next) => {
res.status(500).json({
message: err.message, // may contain DB connection strings
stack: err.stack, // reveals file paths and library versions
sql: err.sql, // Sequelize sometimes attaches the failing query
});
});
// ✅ SAFE
app.use((err, req, res, next) => {
const errorId = crypto.randomUUID();
logger.error({ errorId, err }, "Unhandled error");
res.status(err.statusCode ?? 500).json({
error: err.isOperational ? err.message : "An unexpected error occurred",
errorId, // reference for support — not internal details
});
});
```
**检查清单:**
- [ ] API 响应中绝不能暴露堆栈跟踪信息
- [ ] 错误消息中不包含数据库连接字符串或查询文本
- [ ] 返回唯一的错误 ID,以便在日志中关联和追踪错误
- [ ] 区分操作错误(如验证失败、404)与程序员错误
## 8. 日志与监控
```
// ❌ BAD — logs PII and secrets
logger.info(`User login: ${JSON.stringify(req.body)}`);
// req.body contains: { email: "user@example.com", password: "secret123" }
// ✅ SAFE — log only safe fields
logger.info({ userId: user.id, action: "login", ip: req.ip }, "User logged in");
```
**检查清单:**
- [ ] 绝不记录密码、token 和信用卡号等敏感信息
- [ ] 不逐字记录请求体(可能包含 PII)
- [ ] 日志条目包含用于请求链路追踪的 correlation ID
- [ ] 日志被发送到集中式的日志服务(不仅限于打印到 stdout)
- [ ] 日志格式化程序会对敏感字段进行脱敏处理
## 9. Node.js 专项
```
// ❌ VULNERABLE — prototype pollution
function merge(target, source) {
for (const key in source) {
target[key] = source[key]; // attacker can set __proto__
}
}
// ✅ SAFE
function merge(target, source) {
const safeSource = Object.create(null);
Object.assign(safeSource, source);
for (const key of Object.keys(safeSource)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
target[key] = safeSource[key];
}
}
```
**检查清单:**
- [ ] `child_process.exec()` 不与用户输入一起使用
- [ ] 不使用 `eval()`, `new Function()`, `setTimeout(string)`
- [ ] 在对象合并/扩展工具中防范原型链污染
- [ ] 生产环境不使用 `http` 模块(改用 `https`)
- [ ] 所有公开的 endpoint 均已应用速率限制
- [ ] 使用 Helmet.js(或同等工具)设置安全的 HTTP 标头
## 10. React / 前端专项
**检查清单:**
- [ ] 前端打包文件中无敏感数据(token、密钥、私钥)
- [ ] 添加 `NEXT_PUBLIC_` / `REACT_APP_` 前缀的环境变量必须是真正可公开的信息
- [ ] 在生产构建中禁用 React DevTools
- [ ] 生产环境中不可公开访问 Source maps
- [ ] `localStorage` 和 `sessionStorage` 中不存放敏感 token(应使用 `httpOnly` Cookie)
- [ ] 第三方脚本必须从可信来源加载并附带 SRI 哈希
## 11. TypeScript 安全模式
```
// ❌ Unsafe — bypasses type system
function processInput(data: any) {
data.delete(); // no type check
}
// ✅ Safe — validate before use
function processInput(data: unknown) {
if (!isValidData(data)) throw new Error("Invalid input");
data.delete(); // now type-safe
}
// ❌ Type assertion without validation
const user = JSON.parse(response) as User;
user.deleteAccount(); // no guarantee this is actually a User
// ✅ Zod schema validation
import { z } from "zod";
const UserSchema = z.object({ id: z.string(), email: z.string().email() });
const user = UserSchema.parse(JSON.parse(response)); // throws if invalid
```
**检查清单:**
- [ ] 避免 `any` 类型——应使用 `unknown` 并结合类型守卫
- [ ] 在使用外部数据(API 响应、用户输入)前,使用 Zod/Yup/Joi 进行校验
- [ ] 不对不受信任的数据使用类型断言(`as Type`)
- [ ] `tsconfig.json` 中设置了 `strict: true`
## 12. 支付集成安全
适用于 Razorpay、Stripe 及其他类似的支付提供商。
```
// ❌ VULNERABLE — no signature verification
app.post("/webhook/razorpay", async (req, res) => {
await processPayment(req.body); // attacker can fake payment events
res.json({ received: true });
});
// ✅ SAFE — verify before processing
import crypto from "crypto";
app.post("/webhook/razorpay",
express.raw({ type: "application/json" }), // raw body for signature
async (req, res) => {
const signature = req.headers["x-razorpay-signature"] as string;
const expectedSig = crypto
.createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET!)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
return res.status(400).json({ error: "Invalid signature" });
}
await processPayment(JSON.parse(req.body.toString()));
res.json({ received: true });
}
);
```
**检查清单:**
- [ ] 使用 `crypto.timingSafeEqual()` 验证 Webhook 签名(时序安全比较)
- [ ] 支付金额在服务端进行计算,绝不能基于客户端请求
- [ ] 货币金额以整数(如分/派萨)形式存储,不使用浮点数
- [ ] Webhook 事件具备幂等性(同一事件被处理两次的结果应保持一致)
- [ ] Webhook endpoint 使用 `express.raw()` 中间件(而非 `express.json()`)
- [ ] 详细记录支付失败的信息以便进行对账
## 如何实现自动化
在每个 PR 上手动执行此清单是不可持续的。我通过以下方式实现自动化:
### Mesrai — AI 代码审查
[Mesrai](https://mesrai.com) 会在每个 PR 上自动捕获上述大部分问题。[安全加固规则集](https://marketplace.mesrai.com/library)涵盖了注入、身份验证绕过、敏感数据暴露和错误处理。我还针对支付相关的特定模式添加了自定义规则。
- [开始免费试用(14 天)](https://app.mesrai.com)
- [浏览安全规则](https://marketplace.mesrai.com/library)
- [设置指南](https://docs.mesrai.com)
### ESLint 安全插件
```
npm install --save-dev eslint-plugin-security eslint-plugin-no-secrets
```
```
// .eslintrc.json
{
"plugins": ["security", "no-secrets"],
"extends": ["plugin:security/recommended"],
"rules": {
"no-secrets/no-secrets": "error"
}
}
```
### CI 中运行 npm audit
```
# .github/workflows/security.yml
- name: Security audit
run: npm audit --audit-level=high
```
### Snyk(用于依赖扫描)
```
npx snyk test
npx snyk monitor # continuous monitoring
```
## 参考资源
- [OWASP Top 10](https://owasp.org/Top10/)
- [OWASP Node.js 安全备忘单](https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html)
- [Mesrai 规则库 — 安全](https://marketplace.mesrai.com/library) — 包含 100 多条针对 JS/TS 的安全规则
- [Mesrai](https://mesrai.com) — 在每个 PR 上自动化执行此清单
*由 [@johnboe02](https://github.com/johnboe02) 维护。欢迎提交 Issue 和 PR。*标签:CMS安全, JavaScript, MITM代理, TypeScript, Web安全, 代码审查, 安全插件, 安全检查单, 数据可视化, 蓝队分析