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安全, 代码审查, 安全插件, 安全检查单, 数据可视化, 蓝队分析