plugins/languages/typescript/skills/security/SKILL.md
TypeScript / JavaScript Web 安全编码规范,覆盖 Zod 4 输入验证、CSP3 内容安全策略、Trusted Types、XSS / CSRF 防护、DOMPurify、SameSite cookie、CORS 配置、依赖审计 (pnpm audit / Socket / Snyk)、npm provenance、SubResource Integrity、Permissions Policy、敏感数据日志脱敏、速率限制。Use when 安全加固、漏洞修复、输入校验、安全审计、CSP 配置,或用户提到 "security"、"XSS"、"CSRF"、"CSP"、"CORS"、"input validation"、"audit"、"依赖漏洞"、"sanitize"、"innerHTML"。
npx skillsauth add lazygophers/ccplugin typescript-securityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
本 skill 同时覆盖 JavaScript 项目;示例以 TS 为主,JS 项目去掉类型即可。
边界即防线:所有外部输入 (HTTP body / query / headers / fs / env) 必须 Zod 验证;所有渲染必须经清洗或 React/Vue 自动转义。
typescript-core — 工具链typescript-async — fetch 取消、AbortControllerimport { z } from "zod";
const UserInputSchema = z.object({
name: z.string().min(1).max(100).trim(),
email: z.email().toLowerCase(),
age: z.number().int().min(0).max(150).optional(),
bio: z.string().max(500).optional(),
});
function validate(input: unknown) {
const r = UserInputSchema.safeParse(input);
return r.success
? { ok: true as const, data: r.data }
: { ok: false as const, errors: z.flattenError(r.error).fieldErrors };
}
// API 响应严格校验 (失败抛 ZodError)
async function fetchUser(id: string) {
const r = await fetch(`/api/users/${id}`);
return UserInputSchema.parse(await r.json());
}
// 环境变量启动时 fail-fast
const Env = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
DATABASE_URL: z.url(),
API_KEY: z.string().min(32),
});
export const env = Env.parse(process.env);
// React 自动转义 (默认安全)
<div>{userInput}</div>
// 危险:永不做
// element.innerHTML = userInput;
// <div dangerouslySetInnerHTML={{ __html: userInput }} /> // 仅在 DOMPurify 后
// DOMPurify HTML 白名单清理
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
ALLOWED_ATTR: ["href"],
});
// React
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
// Vue
<div v-html="DOMPurify.sanitize(html)" />
// 纯文本场景 (自动转义)
el.textContent = userInput;
// Trusted Types (Chrome / Edge) — 编译期拒绝裸字符串赋 innerHTML
// CSP: require-trusted-types-for 'script';
const policy = trustedTypes.createPolicy('safe', {
createHTML: (s) => DOMPurify.sanitize(s),
});
el.innerHTML = policy.createHTML(userHtml);
// ✅ 参数化查询 (Drizzle / Prisma 默认安全)
await db.select().from(users).where(eq(users.email, input));
// ❌ 字符串拼接
// await db.execute(`SELECT * FROM users WHERE email = '${input}'`);
Content-Security-Policy:
default-src 'self';
script-src 'self' 'strict-dynamic' 'nonce-{random}';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
require-trusted-types-for 'script';
upgrade-insecure-requests;
report-to csp-endpoint;
// Next.js middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(_req: NextRequest) {
const nonce = crypto.randomUUID();
const csp = [
`default-src 'self'`,
`script-src 'self' 'strict-dynamic' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`require-trusted-types-for 'script'`,
].join("; ");
const res = NextResponse.next();
res.headers.set("Content-Security-Policy", csp);
res.headers.set("X-Frame-Options", "DENY");
res.headers.set("X-Content-Type-Options", "nosniff");
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
return res;
}
strict-dynamic 而非 host 白名单'unsafe-inline' 'unsafe-eval'Report-To / report-uri 收集违规// Hono 4
import { cors } from "hono/cors";
app.use(cors({
origin: ["https://myapp.com"], // 禁 "*" 当 credentials: true
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
}));
// Express 5
import cors from 'cors';
app.use(cors({ origin: /\.example\.com$/, credentials: true }));
// Vite dev 代理 (绕开 CORS)
export default { server: { proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true } } } };
Access-Control-Allow-Origin: * + credentials: trueSet-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/
SameSite=Strict + double-submit token 或 Origin/Sec-Fetch-Site 校验// 禁硬编码 (const apiKey = "sk-xxx"; // 危险!)
// 日志脱敏
const SENSITIVE = new Set(["password", "token", "apikey", "secret", "authorization", "cookie"]);
function sanitize<T extends Record<string, unknown>>(data: T): Partial<T> {
return Object.fromEntries(
Object.entries(data).map(([k, v]) =>
SENSITIVE.has(k.toLowerCase()) ? [k, "***REDACTED***"] : [k, v],
),
) as Partial<T>;
}
pnpm audit --audit-level=high # CI 卡 high+
pnpm audit --fix
pnpm dedupe # 减少重复版本
# 第三方
npx socket@latest scan # Socket.dev 行为级检测
npx snyk test # CVE 数据库
npx better-npm-audit audit # 更严格
# 发布: npm provenance + sigstore (开箱即用)
# package.json: { "publishConfig": { "provenance": true } }
npm publish --provenance --access public
# 锁定: 仅信任锁文件
pnpm install --frozen-lockfile
// SubResource Integrity (CDN 脚本)
// <script src="https://cdn/lib.js" integrity="sha384-..." crossorigin="anonymous"></script>
// Permissions Policy (限关高危 API)
// Permissions-Policy: camera=(), microphone=(), geolocation=(self)
// URL 协议校验 (open redirect / javascript: 协议)
function safeUrl(input: string): string {
const u = new URL(input, location.origin);
if (!['http:', 'https:'].includes(u.protocol)) throw new Error('bad protocol');
return u.toString();
}
// 禁 eval / new Function / setTimeout(string)
// 禁 prototype 污染: 对外 API 用 Object.create(null) 或 Map
import { rateLimiter } from "hono-rate-limiter";
app.use("/api/*", rateLimiter({
windowMs: 60_000,
limit: 100,
standardHeaders: "draft-7",
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "anon",
}));
所有规范在 JS 项目同样适用,去掉 TypeScript 类型注解即可。Zod、DOMPurify、CSP、CORS 都是运行时机制,不依赖 TS。
import { z } from 'zod';
const Login = z.object({
email: z.string().email('邮箱无效'),
password: z.string().min(8, '至少 8 位'),
});
const r = Login.safeParse(form);
if (!r.success) return { errors: r.error.flatten().fieldErrors };
| 现象 | 风险 | 严重 |
|------|------|------|
| 无输入验证 | 注入 / 类型不安全 | 高 |
| innerHTML = userInput | XSS | 高 |
| dangerouslySetInnerHTML / v-html 未清洗 | XSS | 高 |
| eval / new Function(userInput) | 代码注入 | 高 |
| 硬编码密钥 | 凭证泄露 | 高 |
| 无 CSP / 'unsafe-inline' | XSS 防线失效 | 高 |
| Access-Control-Allow-Origin: * + credentials | 跨站请求伪造 | 高 |
| token 放 localStorage | 持久 XSS 风险 | 中 |
| 反射 Origin 无白名单 | CORS 绕过 | 高 |
| 日志输出原始 password / token | 数据泄露 | 高 |
| SQL 字符串拼接 | 注入 | 高 |
| pnpm audit 未跑 | 已知 CVE | 中 |
| 锁文件未提交 | 供应链漂移 | 中 |
innerHTML / 未清洗 dangerouslySetInnerHTML / v-htmlpnpm audit --audit-level=high 卡门pnpm install --frozen-lockfileeval / new Function / 动态 setTimeout(string)tools
--- name: trellisx-workspace description: 维护 `.trellis/task.md` 任务看板 —— trellis 缺的跨任务总览。**一个表格, 一行一个任务**, 列为 id/名称/描述/状态/阶段/进度/worktree (状态/阶段中文显示)。在 task create/start/阶段切换/archive 后**及时更新**对应行; 并**自动清理超 7 天的已完成行**防膨胀。保持看板与 task.json 实时一致。 when_to_use: 维护 / 创建 / 更新 `.trellis/task.md` 任务看板时; task 生命周期任一节点 (create/start/阶段推进/archive) 之后同步看板时; 用户问"当前有哪些任务 / 任务进度 / 任务看板"时。被 trellisx-flow 与 trellisx-apply 注入的流程引用。 user-invocable: true argument-hint: [show|update|sync|cleanup ...] [task id] arguments:
testing
强制以 Trellis task 闭环处理用户指定的请求 (自判新建/并入 → plan→exec→check→finish 全程不跳步)。**仅用户显式主动调用** (/trellisx-flow 或明确要求"强制走 task 处理这个"); **禁止自动 / 被动 / 推断式调用** —— 不要因为某个请求"看起来该建 task"就自动触发本 skill, 那是 apply 注入的 no_task 倾向的职责。
testing
把 强推task + subtask拆分 + worktree隔离 + 闭环收尾 四维度增量注入当前项目 .trellis/ (workflow.md 的 no_task/planning/in_progress 块 + spec 背书文档 + trellis 生命周期 hook worktree 自动化)。强推 task 与闭环为纯 prompt 软约束 (非平台 hook 硬拦截)。**纯增量追加, 绝不替换 trellis 原生文本** (no_task 分类+征同意/check/finish/前缀全保留)。幂等 (marker 包裹)。
development
Claude Code 会话历史整理 — 扫 ~/.claude/projects/**/*.jsonl 全部 session transcripts, 提取学习增量 (用户校正/决策/踩坑/L0 规则) → 全局记忆库 ~/.cortex/.wiki/memory/. 默认 --apply 落盘 (--dry-run opt-in 仅出 JSON plan 预览). 与 cortex-extract (L4-inbox 内部) 互补.