引言
TypeScript 的类型安全防的是「编译期错误」,防不了「运行期攻击」:依赖供应链被投毒、XSS 把用户输入当 HTML 渲染、SQL 拼接出注入、exec 把参数当命令执行、密钥随日志泄漏。本文按攻击面逐一加固:npm audit 与 SCA 管依赖、React/模板的转义管 XSS、参数化查询管 SQL、校验与白名单管命令注入、env 与日志脱敏管敏感信息、CSP 与 zod 管运行时边界,最后给出一份可直接进 review 的 checklist。
前置:/typescript-zod-validation/(运行时校验)、/typescript-runtime-validation-typesafe/(边界校验)、/typescript-nodejs-backend/(服务端攻击面)。
目录
- 1. 安全威胁模型概览
- 2. 依赖供应链:npm audit 与 SCA
- 3. 锁文件与供应链攻击
- 4. XSS 在 TypeScript 中的形态
- 5. SQL 注入与参数化查询
- 6. 命令注入与输入校验
- 7. 敏感信息:密钥、env 与日志脱敏
- 8. 运行时防护:CSP 与 zod 校验
- 9. 安全编码 checklist
- 10. 速查表与一句话记忆
- 延伸阅读
1. 安全威胁模型概览
先摆全攻击面,再谈加固:
| 威胁 | 攻击入口 | 后果 |
|---|---|---|
| 供应链投毒 | 依赖安装/升级 | 远程代码执行、密钥窃取 |
| XSS | 用户输入渲染到 HTML | 会话劫持、敏感数据窃取 |
| SQL 注入 | 查询拼接用户输入 | 数据泄露、篡改、删除 |
| 命令注入 | exec/spawn 拼接参数 | 任意命令执行 |
| 敏感信息泄漏 | 密钥入 code/日志 | 凭据泄露、横向渗透 |
| 越权/篡改 | 入站数据未校验 | 数据完整性与访问控制破坏 |
核心认知:类型系统在编译后消失,所有外部边界都要有运行时防线。威胁模型三条主线:进站(输入校验)、出站(输出转义/参数化)、存储(敏感信息保护)。
2. 依赖供应链:npm audit 与 SCA
供应链是第一攻击面——npm 依赖树动辄上千包,一个漏洞全链路受影响。
npm audit # 扫描已知漏洞
npm audit --omit=dev # 只看生产依赖
npm audit fix # 自动升级到修复版本
npm audit fix --force # 可能破坏兼容的大升级(慎用)
{
"dependencies": { "express": "4.19.2" }, // 精确版本,防 semver 漂移
"overrides": { "minimatch": "^9.0.0" } // 传递依赖强制覆盖(修间接漏洞)
}
SCA(软件成分分析)工程化:CI 门禁(npm audit --audit-level=high 失败即拦截合并);Renovate/Snyk 自动扫描 + 升级 PR;最小依赖原则(每个新依赖问一句「能否自写/用标准库」);生产依赖瘦身(build 工具全放 devDependencies)。坑:npm audit fix 可能升到 breaking 版本——先看 audit 类型,再用 overrides 精确控制间接依赖。
3. 锁文件与供应链攻击
锁文件是供应链的地基——锁住每个依赖的精确版本与完整性哈希:
package-lock.json的integrity字段:安装时校验包 hash,防「包内容被篡改」;- 必须提交进 git:不提交 = 每次安装都可能拉新版本 = 供应链漂移;
- CI 用 frozen:
npm ci/pnpm install --frozen-lockfile,锁文件变了直接失败。
node -e "const l=require('./package-lock.json'); console.log(l.packages['node_modules/lodash'].integrity)"
防投毒策略:registry 白名单只允许官方源;pnpm.onlyBuiltDependencies 禁 postinstall 脚本(恶意包常用它植入代码),只对 esbuild 等白名单放行;新依赖审查 stars/下载量/维护活跃度/是否近期换过 maintainer(抢注信号);npx 慎用——临时拉取包同样有投毒面。坑:yarn 的 --ignore-scripts 与 pnpm 的 onlyBuiltDependencies 不通用,迁移工具链要重配供应链策略。
4. XSS 在 TypeScript 中的形态
XSS 是前端头号注入,转义陷阱在「你以为安全的地方」:
// ❌ dangerouslySetInnerHTML 直接渲染用户输入
function Comment({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// ✅ React 默认转义:文本节点里的 <script> 不当 HTML 执行
function SafeComment({ text }: { text: string }) {
return <div>{text}</div>;
}
// ❌ 拼接进 src:用户输入 "javascript:alert(1)"
function Avatar({ url }: { url: string }) {
return <img src={url} alt="" />;
}
// ✅ URL 协议白名单
export function safeUrl(raw: string): string {
try {
const u = new URL(raw);
return ["http:", "https:"].includes(u.protocol) ? u.href : "";
} catch {
return "";
}
}
XSS 加固清单:默认转义是防线,别为富文本开 dangerouslySetInnerHTML——确实要富文本用 DOMPurify.sanitize;href/src 只允许 http:/https:;服务端模板 {{{ }}}(非转义)与 {{ }} 别混用;CSP(§8)兜底即使漏转义。坑:innerHTML/document.write/eval 在 TS 里同样危险,eslint 的 no-eval/no-unsafe-innerhtml 能编译期拦一半。
5. SQL 注入与参数化查询
注入原理:用户输入拼接进 SQL 改变了查询语义:
// ❌ 字符串拼接
const sql = `SELECT * FROM users WHERE email = '${email}'`; // ' OR '1'='1
db.query(sql);
// ✅ 参数化:值由驱动转义
const safe = await prisma.$queryRaw<{ id: number }[]>`
SELECT id FROM users WHERE email = ${email}
`;
// ✅ ORM where 对象天然参数化
const user = await prisma.user.findFirst({ where: { email } });
防线优先级:ORM 对象查询(不可能注入)→ 模板参数化(值安全,但表名/列名不能进参数)→ raw SQL 逃生舱(白名单映射表名/列名)。ORDER BY/LIMIT 等无法参数化的片段是注入高发区——用白名单或数值强校验:
const ALLOWED = { users: "users", orders: "orders" } as const;
function queryTable(t: keyof typeof ALLOWED) {
return db.query(`SELECT * FROM ${ALLOWED[t]}`); // 白名单内安全
}
6. 命令注入与输入校验
命令注入:用户输入拼进 shell 命令导致任意命令执行:
import { execFile, spawn } from "node:child_process";
// ❌ exec 走 shell,拼接即注入
exec(`git clone ${repoUrl}`); // repoUrl="https://x; rm -rf /"
// ✅ execFile/spawn 不走 shell,参数数组传值
execFile("git", ["clone", repoUrl], { shell: false });
输入校验完整套路:
import { z } from "zod";
const Branch = z.enum(["main", "release", "dev"]); // 白名单 enum 最严
const RepoPattern = z.string().regex(/^[a-z0-9-]+$/); // 极窄字符集
const dto = z.object({ branch: Branch, repo: RepoPattern }).parse(body);
execFile("git", ["checkout", dto.branch]); // 值已白名单
坑:shell: false 之外,参数含特殊字符(;、|、$())仍可能被解释——白名单校验参数比信任 shell 安全得多;路径类输入用 path.basename 剥离目录穿越。
7. 敏感信息:密钥、env 与日志脱敏
密钥泄漏是「一颗老鼠屎坏一锅粥」。
env 管理纪律:
git check-ignore .env # 确认 .env 被忽略
// .gitignore
.env
.env.*
!.env.example
运行时读取:禁止 process.env.API_KEY ?? "sk-xxxx" 注入默认值,用显式校验缺失:
function requireEnv(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`缺少环境变量 ${name}`);
return v;
}
const apiKey = requireEnv("API_KEY");
日志脱敏:
const SENSITIVE_KEYS = ["password", "token", "secret", "apiKey", "authorization"];
export function redact(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(redact);
if (obj && typeof obj === "object") {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) =>
SENSITIVE_KEYS.includes(k.toLowerCase()) ? [k, "[REDACTED]"] : [k, redact(v)]
)
);
}
return obj;
}
// Logger 统一出口,所有日志先过 redact
坑:console.log(req.headers) 直接打 authorization 是最高频泄漏;?token= 也会进 access log——日志采集器也要脱敏。
8. 运行时防护:CSP 与 zod 校验
两道「运行时防线」:CSP 管前端加载资源,zod 管入站数据形状。
CSP——XSS 的最后兜底:
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // 只允许本站脚本
styleSrc: ["'self'", "'unsafe-inline'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
})
);
zod 运行时校验——入站数据在进业务逻辑前定型:
import { z } from "zod";
const CreateOrderSchema = z.object({
items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })),
amount: z.number().positive(),
});
type CreateOrder = z.infer<typeof CreateOrderSchema>;
app.post("/orders", (req, res) => {
const dto = CreateOrderSchema.parse(req.body); // 失败抛 400
service.createOrder(dto);
});
防线定位:CSP 防出站——即使渲染层漏转义,脚本也执行不了;zod 防进站——畸形结构在入口被挡下。坑:CSP 上线会挡内联脚本(如 analytics)——先 Content-Security-Policy-Report-Only 观察报告再切 enforce。
9. 安全编码 checklist
进 review 前的安全自检清单:
- 所有外部输入过运行时校验(zod/schema,非仅 TS 类型)
- SQL 全部参数化/ORM,无字符串拼接;表名列名走白名单
-
exec/spawn走shell:false+ 参数数组,参数白名单校验 - 无
dangerouslySetInnerHTML/innerHTML直接渲染用户输入;富文本过 DOMPurify -
href/src协议白名单,拒绝javascript:/data: -
.env不入 git,密钥只用requireEnv读取 - 日志出口统一脱敏,headers/query 不过 log
- 依赖:
npm audit门禁 + 锁文件提交 + frozen 安装 + postinstall 白名单 - CSP 已配置(先 report-only 后 enforce)
- 越权检查:每个接口验证「登录用户」与「资源归属」
10. 速查表与一句话记忆
| 攻击面 | 防线 |
|---|---|
| 供应链 | npm audit + 锁文件 + onlyBuiltDependencies + SCA |
| XSS | 默认转义 + DOMPurify + 协议白名单 + CSP |
| SQL 注入 | ORM/参数化 + 表名白名单 |
| 命令注入 | execFile/spawn shell:false + 参数白名单 |
| 密钥泄漏 | .env 不提交 + requireEnv + 日志脱敏 |
| 越权/篡改 | zod 入站校验 + 归属检查 |
一句话记忆:安全加固 = 供应链(audit + 锁文件 + 脚本白名单)+ 进站(zod 校验 + 归属检查)+ 出站(转义 + 协议白名单 + CSP 兜底)+ 存储(参数化 + shell:false + 密钥 env 化 + 日志脱敏)——类型安全管编译期,这四道防线管运行期。
延伸阅读
- /typescript-zod-validation/ — 入站数据运行时校验
- /typescript-runtime-validation-typesafe/ — 边界校验与类型安全
- /typescript-nodejs-backend/ — 服务端攻击面与中间件
- /typescript-type-first-development/ — 契约式开发与 schema 先行
- /typescript-error-handling-result/ — 安全相关的错误建模
- Node.js 专题 — 服务端安全与进程
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。