diff --git a/.gitignore b/.gitignore index 2c8a3b9c8..985f9bd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ coverage *.lock *.exe src/utils/vendor/ +.tmp/ # AI tool runtime directories .agents/ diff --git a/biome.json b/biome.json index debb5e3e4..218792f22 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,7 @@ "useIgnoreFile": true }, "files": { - "includes": ["**", "!!**/dist", "!!**/packages/@ant"] + "includes": ["**", "!!**/dist", "!!**/packages/@ant", "!!**/src/costrict/skill/builtin.ts"] }, "formatter": { "enabled": true, diff --git a/package.json b/package.json index e0049bf18..b920187e2 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "scripts/postinstall.cjs" ], "scripts": { + "prebuild": "bun run scripts/generate-skills.ts", "build": "bun run build.ts", "dev": "bun run scripts/dev.ts", "dev:inspect": "bun run scripts/dev-debug.ts", @@ -50,7 +51,8 @@ "check:unused": "knip-bun", "health": "bun run scripts/health-check.ts", "postinstall": "node scripts/postinstall.cjs", - "docs:dev": "npx mintlify dev" + "docs:dev": "npx mintlify dev", + "generate:skills": "bun run scripts/generate-skills.ts" }, "dependencies": {}, "devDependencies": { diff --git a/scripts/generate-skills.ts b/scripts/generate-skills.ts new file mode 100644 index 000000000..b1a84109a --- /dev/null +++ b/scripts/generate-skills.ts @@ -0,0 +1,299 @@ +#!/usr/bin/env bun +/** + * Downloads builtin skills from their source repositories and generates + * src/costrict/skill/builtin.ts with all skill files embedded as string constants. + * + * Usage: bun run scripts/generate-skills.ts + */ + +import fs from 'fs/promises' +import path from 'path' +import { spawnSync } from 'child_process' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const bundledSkillsDir = path.resolve(__dirname, '../.tmp/skills') +const builtinTsFile = path.resolve(__dirname, '../src/costrict/skill/builtin.ts') + +const BUILTIN_SKILLS = { + 'security-review': { + repo: 'zgsm-ai/security-review-skill', + branch: 'main', + subdir: 'security-review', + }, +} as const + +type Index = { + skills: Array<{ + name: string + description: string + files: string[] + }> +} + +async function fetchCommitSha(repo: string, branch: string): Promise { + const apiUrl = `https://api.github.com/repos/${repo}/commits/${branch}` + try { + const response = await fetch(apiUrl, { + headers: { + 'User-Agent': 'csc-build', + Accept: 'application/vnd.github.v3+json', + }, + }) + if (!response.ok) { + console.warn(` ⚠ Could not fetch commit SHA from GitHub API: ${response.status}`) + return null + } + const data = (await response.json()) as { sha?: string } + return data.sha ?? null + } catch (err) { + console.warn(` ⚠ Failed to fetch commit SHA: ${err}`) + return null + } +} + +async function fetchIndex(repo: string, branch: string): Promise { + const indexUrl = `https://raw.githubusercontent.com/${repo}/${branch}/index.json` + const response = await fetch(indexUrl) + if (!response.ok) { + throw new Error(`Failed to fetch index: ${indexUrl} (${response.status})`) + } + return response.json() as Promise +} + +async function fetchFile(url: string): Promise { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to fetch file: ${url} (${response.status})`) + } + return response.text() +} + +async function downloadSkill( + name: string, + config: { repo: string; branch: string; subdir: string }, +): Promise<{ name: string; commitSha: string | null } | null> { + const { repo, branch, subdir } = config + console.log(`\n📦 Downloading skill: ${name}`) + console.log(` From: https://github.com/${repo}`) + console.log(` Branch: ${branch}`) + + // Try HTTP first + try { + return await downloadSkillHttp(name, config) + } catch (httpErr) { + console.warn(` ⚠ HTTP download failed (${httpErr}), trying git clone...`) + return await downloadSkillGit(name, config) + } +} + +async function downloadSkillHttp( + name: string, + config: { repo: string; branch: string; subdir: string }, +): Promise<{ name: string; commitSha: string | null } | null> { + const { repo, branch, subdir } = config + + const commitSha = await fetchCommitSha(repo, branch) + if (commitSha) { + console.log(` Commit: ${commitSha.slice(0, 7)}`) + } + + const index = await fetchIndex(repo, branch) + if (!index?.skills?.length) { + throw new Error(`Invalid index for skill: ${name}`) + } + + const skill = index.skills.find(s => s.name === name) + if (!skill) { + throw new Error(`Skill "${name}" not found in index`) + } + + console.log(` Found ${skill.files.length} files to download`) + + const skillOutputDir = path.join(bundledSkillsDir, name) + await fs.mkdir(skillOutputDir, { recursive: true }) + + const pathPrefix = subdir ? `${subdir}/` : '' + + for (const file of skill.files) { + const url = `https://raw.githubusercontent.com/${repo}/${branch}/${pathPrefix}${file}` + const targetPath = path.join(skillOutputDir, file) + + await fs.mkdir(path.dirname(targetPath), { recursive: true }) + + try { + const content = await fetchFile(url) + await fs.writeFile(targetPath, content, 'utf-8') + console.log(` ✓ ${file}`) + } catch (err) { + console.warn(` ✗ Failed to download ${file}: ${err}`) + } + } + + const skillMdPath = path.join(skillOutputDir, 'SKILL.md') + try { + await fs.access(skillMdPath) + } catch { + throw new Error(`Skill "${name}" missing SKILL.md`) + } + + console.log(` ✓ Skill ${name} downloaded successfully`) + return { name, commitSha } +} + +function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } { + const result = spawnSync('git', args, { encoding: 'utf-8' }) + return { + ok: result.status === 0, + stdout: result.stdout?.trim() ?? '', + stderr: result.stderr?.trim() ?? '', + } +} + +async function downloadSkillGit( + name: string, + config: { repo: string; branch: string; subdir: string }, +): Promise<{ name: string; commitSha: string | null } | null> { + const { repo, branch, subdir } = config + const cloneUrl = `https://github.com/${repo}.git` + const cloneDir = path.join(bundledSkillsDir, `.clone-${name}`) + const skillOutputDir = path.join(bundledSkillsDir, name) + + console.log(` git clone ${cloneUrl}`) + + await fs.rm(cloneDir, { recursive: true, force: true }) + + const cloneResult = git('clone', '--depth', '1', '--branch', branch, cloneUrl, cloneDir) + if (!cloneResult.ok) { + throw new Error(`git clone failed: ${cloneResult.stderr}`) + } + + const shaResult = git('-C', cloneDir, 'rev-parse', 'HEAD') + const commitSha = shaResult.ok ? shaResult.stdout : null + if (commitSha) { + console.log(` Commit: ${commitSha.slice(0, 7)}`) + } + + const srcDir = subdir ? path.join(cloneDir, subdir) : cloneDir + await fs.rm(skillOutputDir, { recursive: true, force: true }) + await fs.cp(srcDir, skillOutputDir, { recursive: true }) + + await fs.rm(cloneDir, { recursive: true, force: true }) + + const skillMdPath = path.join(skillOutputDir, 'SKILL.md') + try { + await fs.access(skillMdPath) + } catch { + throw new Error(`Skill "${name}" missing SKILL.md`) + } + + const fileCount = (await walk(skillOutputDir)).length + console.log(` ✓ ${fileCount} files copied via git clone`) + return { name, commitSha } +} + +async function walk(dir: string, base = ''): Promise { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + const files: string[] = [] + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + const relativePath = base ? path.join(base, entry.name) : entry.name + if (entry.isDirectory()) { + files.push(...(await walk(fullPath, relativePath))) + } else { + files.push(relativePath) + } + } + return files + } catch { + return [] + } +} + +async function generateBuiltinTs( + skills: Array<{ name: string; commitSha: string | null }>, +): Promise { + const outLines: string[] = [ + '// This file is auto-generated by scripts/generate-skills.ts', + '// Do not edit manually', + '', + ] + + const skillEntries: string[] = [] + let fileIdx = 0 + + for (const { name, commitSha } of skills) { + const skillDir = path.join(bundledSkillsDir, name) + const files = await walk(skillDir) + const fileEntries: string[] = [] + + for (const file of files) { + const varName = `SKILL_FILE_${fileIdx++}` + const content = await fs.readFile(path.join(skillDir, file), 'utf-8') + const normalizedPath = file.replaceAll('\\', '/') + outLines.push(`const ${varName} = ${JSON.stringify(content)}`) + fileEntries.push(` ${JSON.stringify(normalizedPath)}: ${varName}`) + } + + skillEntries.push(` ${JSON.stringify(name)}: {\n${fileEntries.join(',\n')}\n }`) + } + + outLines.push('') + outLines.push('export const BUNDLED_SKILLS: Record> = {') + outLines.push(skillEntries.join(',\n')) + outLines.push('}') + outLines.push('') + + const versions = skills + .filter(s => s.commitSha) + .map(s => ` ${JSON.stringify(s.name)}: ${JSON.stringify(s.commitSha)}`) + + outLines.push('export const SKILL_VERSIONS: Record = {') + outLines.push(versions.join(',\n')) + outLines.push('}') + outLines.push('') + + await fs.writeFile(builtinTsFile, outLines.join('\n') + '\n') + console.log(`\n✓ Generated ${builtinTsFile}`) +} + +async function main() { + console.log('\nCSC — Downloading Builtin Skills\n') + + await fs.mkdir(bundledSkillsDir, { recursive: true }) + + const results: Array<{ name: string; commitSha: string | null }> = [] + + for (const [name, config] of Object.entries(BUILTIN_SKILLS)) { + try { + const result = await downloadSkill(name, config) + if (result) results.push(result) + } catch (err) { + // Fall back to cached files in .tmp/skills/ if download fails + const skillDir = path.join(bundledSkillsDir, name) + const cached = await walk(skillDir) + if (cached.length > 0) { + console.warn(` ⚠ Download failed, using cached files for "${name}": ${err}`) + results.push({ name, commitSha: null }) + } else { + console.error(` ✗ Download failed and no cache for "${name}": ${err}`) + } + } + } + + if (results.length > 0) { + await generateBuiltinTs(results) + } else { + console.warn('\n⚠ No skills downloaded. builtin.ts not updated.') + } + + console.log('\n💡 Run `bun run build` to rebuild the CLI.\n') +} + +main().catch(e => { + console.error(e) + process.exit(1) +}) diff --git a/src/costrict/skill/builtin.ts b/src/costrict/skill/builtin.ts new file mode 100644 index 000000000..b40d7c6d7 --- /dev/null +++ b/src/costrict/skill/builtin.ts @@ -0,0 +1,187 @@ +// This file is auto-generated by scripts/generate-skills.ts +// Do not edit manually + +const SKILL_FILE_0 = "# 检查项\r\n请严格按照下列检查项对历史对话记录和输出的报告结果进行检查,逐项确认是否满足,否则你会被扣除十亿美金\r\n\r\n## 推理过程检查\r\n主要对历史对话记录/工具调用历史进行检查\r\n\r\n### 推理幻觉检查\r\n1. 所有代码分析均已闭环,不存在待分析的函数\r\n2. 所有结论均有相关代码举证,不存在假设/猜测的结论(无法获取到代码的情况除外)\r\n\r\n### 代码链路完整性检查\r\n1. 除非已经确认该漏洞不可利用或对应的参数均已防护或校验,否则分析过程必须追溯到解析用户可控输入的代码/函数\r\n2. 如果代码链路中包含安全防护/校验函数,必须获取对应代码进行分析,并得出是否安全的结论。若确认无法触发漏洞,则<代码链路的完整性检查>通过\r\n3. 如果追溯到用户可控输入是HTTP请求, 则必须明确对应的接口URI是什么\r\n4. 当可控输入来源于中间件时(数据库、缓存、消息队列等),查找是否存在设置对应字段内容的代码逻辑\r\n5. 针对结论为不存在安全漏洞的,如果符合下述特定场景,则不必提供完整代码, <代码链路的完整性检查>通过\r\n - 已确认 用户可控输入 为代码中定义的常量或非外部可控的变量\r\n - 已确认 用户可控输入 类型为整形、布尔型或枚举型等无法有效拼接的类型\r\n - 数据流中某个代码片段代码是注释掉的,数据流不完整\r\n - SQL注入漏洞,如果是简单的数值拼接、存在参数化预处理、ORM框架或其他防护\r\n - 服务端请求伪造漏洞(SSRF),如果存在安全防护/校验函数、对应参数是整型或布尔型(不可利用)、或者不能直接控制的,如从配置文件、环境变量、数据库查询等非实际直接可控(需要先能控制对应文件或环境)的方式读取的\r\n - 文件路径穿越、路径注入等文件相关类型的漏洞,已确认不存在文件读写相关操作\r\n - 已经明确多次查找均找不到对应的函数或变量,并结合代码上下文分析说明了函数用途且符合代码逻辑的,则<代码链路的完整性检查>通过\r\n6. 当前分析过程链路覆盖以下链路之一:\r\n - 解析用户输入->污点变量传播->触发漏洞\r\n - 确认无安全的防护/校验函数->污点变量传播->触发漏洞\r\n - 确认已有安全防护/校验函数->无法触发漏洞\r\n - 确认符合不存在安全漏洞的特定场景->无法触发漏洞\r\n7. 如果分析中涉及第三方代码或框架,可以根据框架特性和项目使用模式来分析,不必提供完整代码\r\n \r\n## 报告结构检查\r\n主要对输出的报告进行检查\r\n\r\n### 安全漏洞存在性\r\n1. 存在性结论只允许两类结论:存在/不存在\r\n\r\n### 漏洞利用条件及触发方式\r\n1. 漏洞利用条件和触发方式必须对应,逻辑合理,不要出现相互矛盾的情况,如果存在相互矛盾的情况,则<漏洞利用条件及触发方式>不通过\r\n2. 关键在于校验利用条件中关于污点参数的约束(如数值、正则、枚举类型)和触发方式罗列的内容是否矛盾,有矛盾则<漏洞利用条件及触发方式>不通过,如:正则表达式里过滤了单引号,触发方式就不能有单引号了。\r\n\r\n### 数据流\r\n#### 可控输入限制检查\r\n1. 可控输入为HTTP请求类型时,<数据流>起始点必须为相关接口的URI\r\n2. 可控输入为文件读取类型时,<数据流>起始点必须为对应的文件路径\r\n3. 可控输入来源于中间件时(数据库、缓存、消息队列等),明确设置对应字段的代码逻辑,追踪数据流\r\n4. 如果确认相关污点参数均已有安全防护/校验函数,无法触发漏洞,则<可控输入限制检查>通过\r\n5. 如果结论为不存在安全漏洞,且<代码链路的完整性检查>通过的,则<可控输入限制检查>通过\r\n\r\n#### 覆盖度检查\r\n1. <数据流>中必须覆盖推理分析得到的完整传播链路\r\n\r\n### 关键代码片段\r\n1. <关键代码片段>列举的代码需要和<数据流>中步骤一一对应,不允许有缺漏\r\n\r\n\r\n# 检查项评估输出定义\r\n你需要以下面定义的XML结构输出你的评估结果\r\n\r\n通过/不通过\r\n简要解释通过/不通过原因\r\n下一步行动建议:输出报告/\r\n1. 获取xxx代码,继续进行分析\r\n2. xxx\r\n\r\n\r\n\r\n示例1:\r\n\r\n不通过\r\n当前分析过程未明确HTTP请求对应的接口URI,且可能存在安全防护/校验函数未分析,不满足代码链路完整性检查\r\n下一步行动建议:\r\n1. 获取路由绑定代码,继续进行分析,明确接口URI\r\n2. 获取可能的校验函数validator.check_input_and_output_valid实现,分析其是否足够安全\r\n\r\n\r\n\r\n示例2:\r\n\r\n通过\r\n所有检查项已通过\r\n下一步行动建议:输出报告\r\n\r\n\r\n" +const SKILL_FILE_1 = "# 控制安全建模分析法\r\n\r\n## 核心公式\r\n\r\n缺失类漏洞 = 敏感操作 - 应有控制 或 敏感操作 + 控制失效绕过\r\n\r\n敏感操作存在,但应有的安全控制缺失 = 漏洞\r\n\r\n机制缺陷导致控制绕过 = 漏洞\r\n\r\n## 安全控制建模&控制存在/有效性验证\r\n\r\n### 敏感操作分类\r\n\r\n| 类别 | 识别特征 | 典型示例 | 风险等级 |\r\n| ------------ | ------------------------------------ | --------------------- | -------- |\r\n| **数据修改** | POST/PUT/DELETE | 创建/修改/删除 | 高 |\r\n| **数据访问** | GET + ID参数 | /user/{id} /用户/{id} | 中 |\r\n| **批量操作** | export/download/batch 导出/下载/批量 | 导出/批量删除 | 高 |\r\n| **权限变更** | role/permission/grant 角色/权限/授予 | 角色/权限分配 | 严重 |\r\n| **资金操作** | transfer/pay/refund 转账/付款/退款 | 转账/支付/退款 | 严重 |\r\n| **认证操作** | login/password/token 登录/密码/令牌 | 登录/密码重置 | 严重 |\r\n\r\n### 安全控制矩阵\r\n\r\n| 敏感操作类型 | 必须控制 | 可选控制 |\r\n| ------------ | -------------------------------- | -------- |\r\n| **数据修改** | 认证、授权、资源所有权、输入验证 | 审计日志 |\r\n| **数据访问** | 认证、资源所有权/范围限制 | 数据脱敏 |\r\n| **批量操作** | 认证、授权、数量限制 | 频率限制 |\r\n| **权限变更** | 认证、高级授权、权限边界检查 | 审批流程 |\r\n| **资金操作** | 认证、授权、幂等性、并发控制 | 风控检查 |\r\n| **认证操作** | 幂等性、频率限制 | 并发控制 |\r\n\r\n### 缺失控制 → 漏洞映射\r\n\r\n| 缺失控制 | 漏洞类型 | CWE |\r\n| -------------- | -------------- | ------- |\r\n| 认证缺失 | 认证绕过 | CWE-306 |\r\n| 授权缺失 | 功能级授权缺失 | CWE-862 |\r\n| 资源所有权缺失 | IDOR | CWE-639 |\r\n| 授权逻辑错误 | 授权错误 | CWE-863 |\r\n| 幂等性缺失 | 重放攻击 | CWE-294 |\r\n| 并发控制缺失 | 竞态条件 | CWE-362 |\r\n| 频率限制缺失 | 暴力破解、洪水攻击 | ... |\r\n\r\n### 控制机制缺陷 → 机制绕过\r\n\r\n- **验证码状态未失效**: 验证码校验成功后,排查未将其从缓存中删除或未标记为作废的状态控制遗漏(导致同一个验证码被多次复用)。\r\n- **输入未绑定**: 审查敏感操作二次认证中(如资金转账、修改密码等),目标手机号/邮箱完全信任前端传入参数,未与当前登录用户会话身份进行绑定的逻辑缺陷(导致二次鉴权失效)。\r\n- **认证步骤绕过**:审查多步操作(如找回密码流程:1.验证手机 -> 2.输入验证码 -> 3.重置密码)中,关键步骤仅依赖前端传入参数(如 `step=3`)决定执行流程,而未在后端强校验用户已完成前置步骤的逻辑跳跃漏洞。\r\n- **校验返回错误**: 检查鉴权失败或Token无效的分支中,代码仅记录日志,输出错误信息或重新赋值创建token/session,而**未写 `return`、`raise` 彻底阻断执行流**,导致后续敏感代码继续执行的短路缺陷。\r\n\r\n\r\n## 注意事项\r\n\r\n! 对于认证、授权类问题,注意横向对比同模块的其他方法实现\r\n! 注意分析控制机制是否存在逻辑缺陷,可导致机制被绕过\r\n" +const SKILL_FILE_2 = "# Django Security Audit\r\n\r\n> Django 框架安全审计模块\r\n> 适用于: Django, Django REST Framework\r\n\r\n## 识别特征\r\n\r\n```python\r\n# Django项目识别\r\nimport django\r\nfrom django.http import HttpResponse\r\nfrom django.views import View\r\n\r\n# 文件结构\r\n├── manage.py\r\n├── project/\r\n│ ├── settings.py\r\n│ ├── urls.py\r\n│ └── wsgi.py\r\n├── app/\r\n│ ├── models.py\r\n│ ├── views.py\r\n│ ├── urls.py\r\n│ └── forms.py\r\n└── templates/\r\n```\r\n\r\n---\r\n\r\n## Django特定漏洞\r\n\r\n### 1. ORM注入\r\n\r\n```python\r\n# 危险: raw()拼接\r\nUser.objects.raw(f\"SELECT * FROM auth_user WHERE id = {user_id}\")\r\nUser.objects.raw('SELECT * FROM auth_user WHERE id = %s' % str(id)) # 字符串拼接\r\n\r\n# 危险: extra()拼接\r\nUser.objects.extra(where=[f\"id = {user_id}\"])\r\nUser.objects.extra(WHERE=['id=' + str(id)]) # 拼接导致注入\r\n\r\n# 危险: RawSQL\r\nfrom django.db.models.expressions import RawSQL\r\nqueryset.annotate(val=RawSQL(f\"SELECT col FROM tbl WHERE id = {id}\"))\r\n\r\n# 危险: 参数名可控 (高危!)\r\nimport json\r\ndata = json.loads(request.body.decode())\r\nStudent.objects.filter(**data).first()\r\n# payload: {\"passkey__contains\":\"a\"} 利用 lookup 语法\r\n# 可用 lookup: __contains, __startswith, __endswith, __icontains, __regex 等\r\n\r\n# 危险: 字典键可控\r\ndict = {'username': user_input, 'age': 18}\r\nUser.objects.create(**dict) # 如果 username 包含注入字符\r\n\r\n# 二次注入\r\nfilename = request.GET.get('url')\r\nFile.objects.create(filename=filename) # 存入: ' or '1'='1\r\n# 后续拼接查询\r\ncur = connection.cursor()\r\ncur.execute(\"\"\"select * from file where filename='%s'\"\"\" % filename)\r\n# 产生注入: select * from file where filename='' or '1'='1'\r\n\r\n# Django ORM Lookup 语法\r\n# field__lookup = value\r\n# __exact: 精确匹配\r\n# __contains: 包含\r\n# __startswith: 开始于\r\n# __endswith: 结束于\r\n# __regex: 正则匹配 (需注意 ReDoS)\r\n# __iregex: 不区分大小写正则\r\n\r\n# 安全: 参数化\r\nUser.objects.raw(\"SELECT * FROM auth_user WHERE id = %s\", [user_id])\r\nUser.objects.filter(id=user_id) # ORM 标准 API\r\nUser.objects.get(id=str(id))\r\nUser.objects.extra(WHERE=['id=%s'], params=[str(id)]) # 正确的 extra 用法\r\n\r\n# SQLite3 参数化\r\ncon = sqlite3.connect('sql.db')\r\nc = con.cursor()\r\nusername = c.execute('SELECT name FROM users WHERE id = ?', [id]).fetchone()[0]\r\n```\r\n\r\n### 2. 模板XSS\r\n\r\n```python\r\n# 危险: mark_safe\r\nfrom django.utils.safestring import mark_safe\r\nmark_safe(f\"
{user_input}
\")\r\n\r\n# 危险: |safe过滤器\r\n{{ user_input|safe }}\r\n\r\n# 危险: autoescape off\r\n{% autoescape off %}{{ user_input }}{% endautoescape %}\r\n\r\n# 危险: 直接拼接 HTML\r\nname = request.GET.get('name')\r\nreturn HttpResponse(\"

name: %s

\" % name)\r\n\r\n# 安全: 默认转义\r\n{{ user_input }} # 自动转义\r\n\r\n# 安全: 使用 render\r\nfrom django.shortcuts import render\r\nreturn render(request, 'index.html', {'name': name})\r\n```\r\n\r\n### 2.5. SSTI / 格式化字符串注入\r\n\r\n```python\r\n# 危险: 双重格式化字符串\r\nname = request.GET.get('name')\r\ntemplate = \"

user:{user}, name:%s

\" % name # 第一次格式化\r\nreturn HttpResponse(template.format(user=request.user)) # 第二次格式化\r\n# payload: name = \"{user.password}\" → 泄露用户密码\r\n\r\n# 信息泄露 payload\r\n{user.password} # 读取密码哈希\r\n{user.__init__.__globals__[__builtins__][eval]} # 获取 eval 函数\r\n\r\n# 读取 SECRET_KEY 路径\r\n{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}\r\n{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}\r\n{user.groups.model._meta.apps.app_configs[auth].module.middleware.settings.SECRET_KEY}\r\n{user.groups.model._meta.apps.app_configs[sessions].module.middleware.settings.SECRET_KEY}\r\n{user.groups.model._meta.apps.app_configs[staticfiles].module.utils.settings.SECRET_KEY}\r\n\r\n# 枚举 app_configs\r\n{user.groups.model._meta.apps.app_configs}\r\n\r\n# format() 限制\r\n# 只支持点 (.) 和中括号 ([])\r\n# 不支持括号调用,因此 RCE 困难\r\n# 主要用于信息泄露\r\n\r\n# 安全: 避免双重格式化\r\nreturn render(request, 'template.html', {'name': name})\r\n```\r\n\r\n### 3. CSRF配置\r\n\r\n```python\r\n# 危险: 全局禁用\r\nMIDDLEWARE = [\r\n # 'django.middleware.csrf.CsrfViewMiddleware', # 被注释\r\n]\r\n\r\n# 危险: 视图级别禁用\r\nfrom django.views.decorators.csrf import csrf_exempt\r\n\r\n@csrf_exempt # 搜索这个装饰器\r\ndef vulnerable_view(request):\r\n ...\r\n\r\n# 危险: CSRF_TRUSTED_ORIGINS过宽\r\nCSRF_TRUSTED_ORIGINS = ['https://*.example.com']\r\n```\r\n\r\n### 4. Session安全\r\n\r\n```python\r\n# settings.py 检查项\r\nSESSION_COOKIE_SECURE = True # 必须为True (HTTPS)\r\nSESSION_COOKIE_HTTPONLY = True # 必须为True\r\nSESSION_COOKIE_SAMESITE = 'Lax' # 或 'Strict'\r\nCSRF_COOKIE_SECURE = True\r\n\r\n# Session存储\r\nSESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'\r\n# 签名cookie需要强SECRET_KEY\r\n```\r\n\r\n### 5. 文件处理\r\n\r\n```python\r\n# 危险: 不安全的文件路径\r\ndef download(request, filename):\r\n file_path = os.path.join(UPLOAD_DIR, filename) # 路径遍历\r\n return FileResponse(open(file_path, 'rb'))\r\n\r\n# 危险: 未验证的文件类型\r\ndef upload(request):\r\n file = request.FILES['file']\r\n with open(f'/uploads/{file.name}', 'wb') as f: # 文件名未过滤\r\n for chunk in file.chunks():\r\n f.write(chunk)\r\n\r\n# 危险: 未验证文件大小\r\nfile = request.FILES.get('filename')\r\nname = os.path.join(UPLOAD_DIR, file.name)\r\nwith open(name, 'wb') as f:\r\n f.write(file.read()) # 可能导致 DoS\r\n\r\n# 安全: 文件类型和大小验证\r\nALLOWED_EXTENSIONS = {'pdf', 'doc', 'docx', 'jpg', 'png'}\r\nMAX_FILE_SIZE = 2097152 # 2MB\r\n\r\ndef allowed_file(filename):\r\n return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS\r\n\r\ndef upload(request):\r\n if request.method == 'POST':\r\n img = request.FILES.get('filename')\r\n if img.size < MAX_FILE_SIZE and allowed_file(img.name):\r\n # 重命名文件 (UUID)\r\n import uuid\r\n ext = img.name.rsplit('.', 1)[1]\r\n new_name = str(uuid.uuid5(uuid.NAMESPACE_DNS, img.name)) + \".\" + ext\r\n\r\n name = os.path.join(UPLOAD_DIR, new_name)\r\n with open(name, 'wb') as f:\r\n for chunk in img.chunks():\r\n f.write(chunk)\r\n return render(request, 'upload.html', {'file': '上传成功'})\r\n else:\r\n return render(request, 'upload.html', {'file': \"不允许的类型或大小超限\"})\r\n\r\n# 安全: 使用 FileField 验证\r\nfrom django.core.validators import FileExtensionValidator\r\n\r\nclass Document(models.Model):\r\n file = models.FileField(\r\n upload_to='documents/%Y/%m/%d', # 按日期组织\r\n validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc'])],\r\n max_length=100\r\n )\r\n\r\n# 安全: 路径遍历防护\r\ndef safe_download(request, filename):\r\n base_dir = os.path.abspath(UPLOAD_DIR)\r\n file_path = os.path.abspath(os.path.join(base_dir, filename))\r\n\r\n if not file_path.startswith(base_dir):\r\n raise ValueError(\"Path traversal detected\")\r\n\r\n return FileResponse(open(file_path, 'rb'))\r\n```\r\n\r\n### 6. 调试模式泄露\r\n\r\n```python\r\n# settings.py\r\nDEBUG = True # 生产环境必须False!!!\r\nALLOWED_HOSTS = ['*'] # 危险: 应指定具体域名\r\n\r\n# 调试工具栏\r\nINSTALLED_APPS = [\r\n 'debug_toolbar', # 生产环境应移除\r\n]\r\n```\r\n\r\n### 7. SECRET_KEY安全\r\n\r\n```python\r\n# 危险: 硬编码或弱密钥\r\nSECRET_KEY = 'django-insecure-xxx'\r\nSECRET_KEY = 'secret'\r\n\r\n# 安全: 环境变量\r\nSECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')\r\n```\r\n\r\n### 8. 管理后台安全\r\n\r\n```python\r\n# urls.py - 检查admin路径\r\nurlpatterns = [\r\n path('admin/', admin.site.urls), # 默认路径,应更改\r\n]\r\n\r\n# 应使用不可预测的路径\r\npath('super-secret-admin-xyz/', admin.site.urls),\r\n\r\n# 检查admin权限\r\n# admin.py\r\n@admin.register(SensitiveModel)\r\nclass SensitiveModelAdmin(admin.ModelAdmin):\r\n # 检查是否限制了权限\r\n def has_delete_permission(self, request, obj=None):\r\n return request.user.is_superuser\r\n```\r\n\r\n---\r\n\r\n## Django审计清单\r\n\r\n```\r\n配置检查:\r\n- [ ] DEBUG = False (生产环境)\r\n- [ ] SECRET_KEY 强度和存储 (避免硬编码)\r\n- [ ] ALLOWED_HOSTS 配置 (避免 ['*'])\r\n- [ ] SESSION_COOKIE_SECURE = True\r\n- [ ] SESSION_COOKIE_HTTPONLY = True\r\n- [ ] SESSION_COOKIE_SAMESITE = 'Lax'\r\n- [ ] CSRF_COOKIE_SECURE = True\r\n- [ ] CSRF 中间件启用\r\n- [ ] debug_toolbar 生产环境移除\r\n\r\nORM 安全:\r\n- [ ] 搜索 .raw() 调用 (字符串拼接)\r\n- [ ] 搜索 .extra() 调用 (where 拼接)\r\n- [ ] 搜索 RawSQL 使用 (拼接)\r\n- [ ] 搜索 cursor.execute() 拼接\r\n- [ ] 检查 .filter(**dict) 参数名可控\r\n- [ ] 检查 .create(**dict) 字典键可控\r\n- [ ] 检查 __regex/__iregex 输入\r\n- [ ] 检查二次注入场景\r\n- [ ] 验证 lookup 语法使用 (__contains 等)\r\n\r\n模板安全:\r\n- [ ] 搜索 mark_safe 使用\r\n- [ ] 搜索 |safe 过滤器\r\n- [ ] 搜索 autoescape off\r\n- [ ] 搜索 HttpResponse 拼接 HTML\r\n- [ ] 检查双重格式化 (% + format)\r\n\r\nCSRF:\r\n- [ ] 检查中间件配置\r\n- [ ] 搜索 @csrf_exempt 装饰器\r\n- [ ] 验证 AJAX 请求 CSRF 处理\r\n- [ ] 检查 CSRF_TRUSTED_ORIGINS 配置\r\n\r\n认证:\r\n- [ ] 检查密码验证器配置\r\n- [ ] 审计自定义认证后端\r\n- [ ] 检查登录限流\r\n- [ ] 验证密码重置流程\r\n\r\n文件处理:\r\n- [ ] 检查 MEDIA_ROOT 配置\r\n- [ ] 验证上传文件类型限制\r\n- [ ] 验证上传文件大小限制\r\n- [ ] 检查文件下载路径验证\r\n- [ ] 验证文件名重命名\r\n- [ ] 检查 FileField validators\r\n\r\n管理后台:\r\n- [ ] 检查 admin 路径 (避免默认 /admin/)\r\n- [ ] 验证 admin 权限配置\r\n- [ ] 检查 ModelAdmin 权限方法\r\n- [ ] 验证敏感模型访问控制\r\n\r\n其他:\r\n- [ ] 搜索 redirect() 用户输入\r\n- [ ] 检查 is_safe_url 使用 (CVE-2017-7233)\r\n- [ ] 验证环境变量使用 (敏感信息)\r\n```\r\n\r\n---\r\n\r\n## 审计正则\r\n\r\n```regex\r\n# ORM 注入\r\n\\.raw\\s*\\(f['\"']|\\.raw\\s*\\([^)]*%\\s*str\\(\r\n\\.extra\\s*\\([^)]*WHERE\\s*=\\s*\\[|\\.extra\\s*\\([^)]*where\\s*=\r\nRawSQL\\s*\\(f['\"']\r\ncursor\\.execute\\s*\\([^)]*['\"]\\s*%|execute\\s*\\([^)]*\\+\r\n\\.filter\\s*\\(\\*\\*|\\.create\\s*\\(\\*\\*\r\n\r\n# XSS\r\nmark_safe\\s*\\(|\\|safe\\s*}}|autoescape\\s+off\r\nHttpResponse\\s*\\([^)]*%|HttpResponse\\s*\\(f['\"']\r\n\r\n# SSTI / 格式化字符串\r\n['\"]\\s*%\\s*[^'\"]*\\.format\\s*\\(\r\n\r\n# CSRF\r\n@csrf_exempt|csrf_exempt\r\nCsrfViewMiddleware.*#\r\n\r\n# 配置\r\nDEBUG\\s*=\\s*True|ALLOWED_HOSTS\\s*=\\s*\\[['\"]?\\*['\"]?\\]\r\n\r\n# SECRET_KEY\r\nSECRET_KEY\\s*=\\s*['\"][^'\"]{0,30}['\"]\r\n\r\n# 文件操作\r\nrequest\\.FILES\\s*\\[|request\\.FILES\\.get\\s*\\(\r\nFileResponse\\s*\\(open\\s*\\(|open\\s*\\([^)]*file\\.name\r\n\r\n# URL 重定向\r\nredirect\\s*\\([^)]*request\\.|HttpResponseRedirect\\s*\\([^)]*request\\.\r\nis_safe_url\\s*\\(\r\n```\r\n\r\n## 最小 PoC 示例\r\n```bash\r\n# DEBUG/ALLOWED_HOSTS 检查\r\ncurl -I http://localhost:8000/ | grep \"X-Content-Type-Options\"\r\n\r\n# SQL 注入 (.raw)\r\ncurl \"http://localhost:8000/users?q=1' OR '1'='1\"\r\n\r\n# Open Redirect\r\ncurl \"http://localhost:8000/redirect?next=http://evil.com\"\r\n```\r\n" +const SKILL_FILE_3 = "# ASP.NET Core / Blazor Security Audit Guide\r\n\r\n> ASP.NET Core 框架安全审计模块\r\n> 适用于: ASP.NET Core 6/7/8, Blazor Server/WebAssembly, Razor Pages, Minimal APIs\r\n\r\n## 识别特征\r\n\r\n```csharp\r\n// ASP.NET Core 项目识别\r\n\r\nusing Microsoft.AspNetCore;\r\n\r\n// 文件结构\r\n├── Program.cs / Startup.cs\r\n├── appsettings.json\r\n├── Controllers/\r\n├── Pages/ (Razor Pages)\r\n├── Views/ (MVC)\r\n├── Models/\r\n├── Data/ (EF Core DbContext)\r\n└── wwwroot/\r\n```\r\n\r\n---\r\n\r\n## SQL 注入检测\r\n\r\n```csharp\r\n// 危险: EF Core 原始SQL拼接\r\nvar users = db.Users\r\n .FromSqlRaw(\"SELECT * FROM Users WHERE Name = '\" + name + \"'\"); // ❌ Critical\r\n\r\n// 危险: Dapper 字符串拼接\r\nconnection.Query(\"SELECT * FROM Users WHERE Id = \" + id); // ❌ Critical\r\nconnection.Execute($\"DELETE FROM Users WHERE Id = {id}\"); // ❌ Critical\r\n\r\n// 危险: ADO.NET 原始拼接\r\nvar cmd = new SqlCommand(\"SELECT * FROM Users WHERE Name = '\" + input + \"'\", conn); // ❌\r\n\r\n// 危险: EF Core ExecuteSqlRaw 拼接\r\ndb.Database.ExecuteSqlRaw(\"UPDATE Users SET Name = '\" + name + \"' WHERE Id = \" + id); // ❌\r\n\r\n// 审计正则\r\nFromSqlRaw\\s*\\(.*\\+|ExecuteSqlRaw\\s*\\(.*\\+\r\nconnection\\.(Query|Execute)\\s*\\(.*\\+|SqlCommand\\s*\\(.*\\+\r\n\r\n// 安全: 参数化查询\r\ndb.Users.FromSqlInterpolated($\"SELECT * FROM Users WHERE Name = {name}\"); // ✓\r\ndb.Database.ExecuteSqlInterpolated($\"UPDATE Users SET Name = {name}\"); // ✓\r\n\r\n// Dapper 参数化\r\nconnection.Query(\"SELECT * FROM Users WHERE Id = @Id\", new { Id = id }); // ✓\r\n\r\n// ADO.NET 参数化\r\nvar cmd = new SqlCommand(\"SELECT * FROM Users WHERE Name = @Name\", conn);\r\ncmd.Parameters.AddWithValue(\"@Name\", input); // ✓\r\n```\r\n\r\n---\r\n\r\n## XSS 检测 (Razor Views)\r\n\r\n```html\r\n\r\n@Html.Raw(Model.UserComment) \r\n@Html.Raw(ViewBag.Message) \r\n\r\n\r\n \r\n\r\n\r\nHtml\\.Raw\\s*\\(|@Html\\.Raw|@\\(.*Html\\.Raw\r\n \r\n```\r\n\r\n---\r\n\r\n## CSRF 配置检测\r\n\r\n```csharp\r\n// 危险: 全局禁用防伪验证\r\nservices.AddControllersWithViews(options =>\r\n{\r\n // 未添加 AutoValidateAntiforgeryTokenAttribute\r\n});\r\n\r\n// 危险: 控制器级别忽略\r\n[IgnoreAntiforgeryToken] // ❌ 跳过CSRF验证\r\npublic IActionResult UpdateProfile(ProfileModel model) { ... }\r\n\r\n// 危险: Razor Pages 禁用\r\n@attribute [IgnoreAntiforgeryToken] // ❌\r\n\r\n// 审计正则\r\n\\[IgnoreAntiforgeryToken\\]|IgnoreAntiforgeryToken\r\nAddControllersWithViews(?!.*AutoValidateAntiforgeryToken)\r\n\r\n// 安全: 全局启用\r\nservices.AddControllersWithViews(options =>\r\n{\r\n options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); // ✓\r\n});\r\n\r\n// Razor Pages 默认启用; API使用JWT则可禁用CSRF但需确认\r\n```\r\n\r\n---\r\n\r\n## 授权配置缺陷\r\n\r\n```csharp\r\n// 危险: [AllowAnonymous] 覆盖 [Authorize]\r\n[Authorize(Roles = \"Admin\")]\r\npublic class AdminController : Controller\r\n{\r\n [AllowAnonymous] // ❌ Critical: 覆盖了类级别的Authorize\r\n public IActionResult SensitiveAction() { ... }\r\n}\r\n\r\n// 危险: 缺少授权属性\r\npublic class PaymentController : Controller // ❌ 无[Authorize]\r\n{\r\n public IActionResult ProcessPayment() { ... }\r\n}\r\n\r\n// 危险: Minimal API 遗漏授权\r\napp.MapGet(\"/admin/users\", GetAllUsers); // ❌ 未添加RequireAuthorization\r\n\r\n// 审计正则\r\n\\[AllowAnonymous\\]|AllowAnonymous\r\nMapGet\\(|MapPost\\(|MapPut\\(|MapDelete\\( // 检查是否有RequireAuthorization\r\n\r\n// 安全: Fallback策略 (默认拒绝)\r\nbuilder.Services.AddAuthorization(options =>\r\n{\r\n options.FallbackPolicy = new AuthorizationPolicyBuilder()\r\n .RequireAuthenticatedUser()\r\n .Build(); // ✓ 未标注的端点默认需要认证\r\n});\r\n\r\n// Minimal API 授权\r\napp.MapGet(\"/admin/users\", GetAllUsers).RequireAuthorization(\"AdminPolicy\"); // ✓\r\n```\r\n\r\n---\r\n\r\n## 不安全的反序列化\r\n\r\n```csharp\r\n// 危险: BinaryFormatter (已弃用,但仍在老代码中)\r\nBinaryFormatter formatter = new BinaryFormatter();\r\nobject obj = formatter.Deserialize(stream); // ❌ Critical: RCE\r\n\r\n// 危险: Newtonsoft TypeNameHandling\r\nvar settings = new JsonSerializerSettings\r\n{\r\n TypeNameHandling = TypeNameHandling.All // ❌ Critical: 反序列化任意类型\r\n};\r\nJsonConvert.DeserializeObject(json, settings);\r\n\r\n// 危险: TypeNameHandling.Auto/Objects 同样可利用\r\nTypeNameHandling = TypeNameHandling.Auto // ❌\r\nTypeNameHandling = TypeNameHandling.Objects // ❌\r\n\r\n// 审计正则\r\nBinaryFormatter|ObjectStateFormatter|SoapFormatter|NetDataContractSerializer\r\nTypeNameHandling\\s*=\\s*TypeNameHandling\\.(All|Auto|Objects|Arrays)\r\nLosFormatter|XmlSerializer\\s*\\(.*typeof\\s*\\(.*GetType\r\n\r\n// 安全: 使用 System.Text.Json (默认安全)\r\nvar obj = JsonSerializer.Deserialize(json); // ✓ 无多态类型处理\r\n\r\n// 如果必须用 Newtonsoft, 使用 SerializationBinder 白名单\r\nvar settings = new JsonSerializerSettings\r\n{\r\n TypeNameHandling = TypeNameHandling.Auto,\r\n SerializationBinder = new KnownTypesBinder(allowedTypes) // ✓\r\n};\r\n```\r\n\r\n---\r\n\r\n## 路径遍历 (文件下载)\r\n\r\n```csharp\r\n// 危险: 直接拼接文件路径\r\n[HttpGet(\"download\")]\r\npublic IActionResult Download(string fileName)\r\n{\r\n var path = Path.Combine(_uploadDir, fileName);\r\n return PhysicalFile(path, \"application/octet-stream\"); // ❌ ../../../etc/passwd\r\n}\r\n\r\n// 危险: 未规范化路径\r\nvar fullPath = _baseDir + \"/\" + userInput; // ❌ 路径遍历\r\nSystem.IO.File.ReadAllText(fullPath);\r\n\r\n// 审计正则\r\nPhysicalFile\\s*\\(.*\\+|Path\\.Combine\\s*\\(.*fileName|File\\.(Read|Open|Delete).*\\+\r\nSendFileAsync\\s*\\(.*\\+\r\n\r\n// 安全: 路径验证\r\n[HttpGet(\"download\")]\r\npublic IActionResult Download(string fileName)\r\n{\r\n if (fileName.Contains(\"..\") || Path.IsPathRooted(fileName))\r\n return BadRequest();\r\n\r\n var path = Path.Combine(_uploadDir, fileName);\r\n var fullPath = Path.GetFullPath(path);\r\n\r\n if (!fullPath.StartsWith(Path.GetFullPath(_uploadDir))) // ✓ 规范化后比对\r\n return BadRequest();\r\n\r\n return PhysicalFile(fullPath, \"application/octet-stream\");\r\n}\r\n```\r\n\r\n---\r\n\r\n## CORS 配置缺陷\r\n\r\n```csharp\r\n// 危险: 允许所有来源 + 凭据\r\nbuilder.Services.AddCors(options =>\r\n{\r\n options.AddPolicy(\"AllowAll\", policy =>\r\n {\r\n policy.AllowAnyOrigin() // ❌\r\n .AllowAnyMethod()\r\n .AllowAnyHeader()\r\n .AllowCredentials(); // ❌ 与AllowAnyOrigin冲突且危险\r\n });\r\n});\r\n\r\n// 危险: 在 Program.cs/Startup.cs 中动态反射 Origin\r\napp.UseCors(policy => policy\r\n .SetIsOriginAllowed(_ => true) // ❌ 允许任何来源\r\n .AllowCredentials());\r\n\r\n// 审计正则\r\nAllowAnyOrigin|SetIsOriginAllowed.*true|AllowCredentials\r\n\r\n// 安全: 明确白名单\r\nbuilder.Services.AddCors(options =>\r\n{\r\n options.AddPolicy(\"Strict\", policy =>\r\n {\r\n policy.WithOrigins(\"https://app.example.com\") // ✓\r\n .WithMethods(\"GET\", \"POST\")\r\n .WithHeaders(\"Content-Type\", \"Authorization\")\r\n .AllowCredentials();\r\n });\r\n});\r\n```\r\n\r\n---\r\n\r\n## Blazor 安全差异\r\n\r\n```csharp\r\n// Blazor Server: 服务端渲染, SignalR连接\r\n// - 敏感数据在服务端, 但 SignalR 消息可能泄露\r\n// - Circuit 劫持: 确保认证检查在每个Hub调用中\r\n// - 防止 JS Interop 注入\r\n\r\n// 危险: Blazor Server 中直接访问数据库无权限检查\r\n@code {\r\n private async Task LoadData()\r\n {\r\n data = await _dbContext.SensitiveData.ToListAsync(); // ❌ 无用户权限检查\r\n }\r\n}\r\n\r\n// 危险: MarkupString 等价于 Html.Raw\r\n@((MarkupString)userInput) // ❌ XSS\r\n\r\n// Blazor WebAssembly: 客户端运行\r\n// - 所有C#代码在浏览器中, 可被反编译 → 不要在WASM中存放密钥\r\n// - API调用必须在服务端验证 → 不能仅依赖客户端授权检查\r\n// - 审计: 检查 wwwroot/ 中是否有敏感配置\r\n\r\n// 审计正则\r\nMarkupString\\)|@\\(\\(MarkupString\\)\r\nNavigationManager\\.NavigateTo\\s*\\(.*\\+ // 开放重定向\r\n```\r\n\r\n---\r\n\r\n## 搜索模式汇总\r\n\r\n```regex\r\n# SQL注入\r\nFromSqlRaw\\s*\\(.*\\+|ExecuteSqlRaw\\s*\\(.*\\+|SqlCommand\\s*\\(.*\\+\r\nconnection\\.(Query|Execute)\\s*\\(.*\\+\r\n\r\n# XSS\r\nHtml\\.Raw\\s*\\(|MarkupString\\)\r\n\r\n# CSRF\r\n\\[IgnoreAntiforgeryToken\\]\r\n\r\n# 授权\r\n\\[AllowAnonymous\\]\r\nMapGet\\(|MapPost\\( // 检查Minimal API授权\r\n\r\n# 反序列化\r\nBinaryFormatter|TypeNameHandling\\.(All|Auto|Objects)\r\n\r\n# 路径遍历\r\nPhysicalFile\\s*\\(.*\\+|Path\\.Combine\\s*\\(.*fileName\r\n\r\n# CORS\r\nAllowAnyOrigin|SetIsOriginAllowed.*true\r\n\r\n# 敏感配置\r\n\"ConnectionStrings\".*password|appsettings.*Development.*json\r\n```\r\n\r\n---\r\n\r\n## 快速审计检查清单\r\n\r\n```markdown\r\n[ ] 检查 ASP.NET Core 版本和已知CVE\r\n[ ] 搜索 FromSqlRaw/ExecuteSqlRaw 字符串拼接\r\n[ ] 搜索 Dapper Query/Execute 拼接\r\n[ ] 搜索 Html.Raw / MarkupString (XSS)\r\n[ ] 搜索 [IgnoreAntiforgeryToken] (CSRF绕过)\r\n[ ] 搜索 [AllowAnonymous] 在敏感Controller上\r\n[ ] 检查 FallbackPolicy 授权配置\r\n[ ] 搜索 BinaryFormatter / TypeNameHandling (反序列化)\r\n[ ] 检查文件下载端点路径验证\r\n[ ] 搜索 AllowAnyOrigin / SetIsOriginAllowed (CORS)\r\n[ ] 检查 appsettings.json 中硬编码密钥/连接字符串\r\n[ ] 检查 Blazor 中 MarkupString 和 JS Interop\r\n[ ] 验证 Minimal API 端点是否有 RequireAuthorization\r\n[ ] 检查异常处理是否泄露堆栈 (app.UseDeveloperExceptionPage)\r\n```\r\n\r\n---\r\n\r\n## 最小 PoC 示例\r\n```bash\r\n# SQL 注入 (Dapper/Raw SQL)\r\ncurl \"http://localhost:5000/api/users?name=admin'OR'1'='1\"\r\n\r\n# 路径遍历\r\ncurl \"http://localhost:5000/download?fileName=../../../etc/passwd\"\r\n\r\n# 开放重定向\r\ncurl \"http://localhost:5000/redirect?url=https://evil.com\"\r\n```\r\n\r\n---\r\n\r\n## 参考资源\r\n\r\n- [Microsoft ASP.NET Core Security Documentation](https://learn.microsoft.com/en-us/aspnet/core/security/)\r\n- [OWASP .NET Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/DotNet_Security_Cheat_Sheet.html)\r\n- [Blazor Security Considerations](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/)\r\n" +const SKILL_FILE_4 = "# Express.js Security Audit\r\n\r\n> Express.js 框架安全审计模块\r\n> 适用于: Express, Koa, Node.js 应用\r\n\r\n## 识别特征\r\n\r\n```javascript\r\n// Express项目识别\r\nconst express = require('express');\r\nconst app = express();\r\n\r\n// 文件结构\r\n├── package.json\r\n├── app.js / index.js\r\n├── routes/\r\n├── controllers/\r\n├── models/\r\n├── middleware/\r\n└── views/\r\n```\r\n\r\n---\r\n\r\n## Express特定漏洞\r\n\r\n### 1. NoSQL注入 (MongoDB)\r\n\r\n```javascript\r\n// 危险: 直接使用用户输入\r\napp.post('/login', async (req, res) => {\r\n const user = await User.findOne({\r\n username: req.body.username,\r\n password: req.body.password // 可注入 {$gt: \"\"}\r\n });\r\n});\r\n\r\n// 攻击payload\r\n// {\"username\": \"admin\", \"password\": {\"$gt\": \"\"}}\r\n\r\n// 安全: 类型验证\r\napp.post('/login', async (req, res) => {\r\n if (typeof req.body.username !== 'string' ||\r\n typeof req.body.password !== 'string') {\r\n return res.status(400).json({error: 'Invalid input'});\r\n }\r\n const user = await User.findOne({...});\r\n});\r\n```\r\n\r\n### 2. 原型污染\r\n\r\n```javascript\r\n// 危险: 合并用户输入\r\nconst _ = require('lodash');\r\napp.post('/config', (req, res) => {\r\n _.merge(config, req.body); // 原型污染!\r\n});\r\n\r\n// 攻击payload\r\n// {\"__proto__\": {\"isAdmin\": true}}\r\n\r\n// 危险: 递归赋值\r\nfunction merge(target, source) {\r\n for (let key in source) {\r\n target[key] = source[key]; // 污染Object.prototype\r\n }\r\n}\r\n\r\n// 安全: 过滤危险键\r\nconst FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype'];\r\nfunction safeMerge(target, source) {\r\n for (let key in source) {\r\n if (FORBIDDEN_KEYS.includes(key)) continue;\r\n target[key] = source[key];\r\n }\r\n}\r\n```\r\n\r\n### 3. 路径遍历\r\n\r\n```javascript\r\n// 危险: 用户输入拼接路径\r\napp.get('/download', (req, res) => {\r\n const file = req.query.file;\r\n res.sendFile(`/uploads/${file}`); // ../../etc/passwd\r\n});\r\n\r\n// 安全: 使用path.basename\r\nconst path = require('path');\r\napp.get('/download', (req, res) => {\r\n const file = path.basename(req.query.file); // 移除路径\r\n const fullPath = path.join('/uploads', file);\r\n // 验证在目标目录内\r\n if (!fullPath.startsWith('/uploads/')) {\r\n return res.status(400).send('Invalid path');\r\n }\r\n res.sendFile(fullPath);\r\n});\r\n```\r\n\r\n### 4. 命令注入\r\n\r\n```javascript\r\n// 危险: exec拼接\r\nconst { exec } = require('child_process');\r\napp.get('/ping', (req, res) => {\r\n exec(`ping -c 1 ${req.query.host}`, (err, stdout) => { // RCE!\r\n res.send(stdout);\r\n });\r\n});\r\n\r\n// 安全: 使用execFile + 参数数组\r\nconst { execFile } = require('child_process');\r\napp.get('/ping', (req, res) => {\r\n execFile('ping', ['-c', '1', req.query.host], (err, stdout) => {\r\n res.send(stdout);\r\n });\r\n});\r\n```\r\n\r\n### 5. XSS (模板引擎)\r\n\r\n```javascript\r\n// EJS危险模式\r\n<%- userInput %> // 不转义\r\n\r\n// 安全模式\r\n<%= userInput %> // 自动转义\r\n\r\n// Pug/Jade危险模式\r\n!{userInput} // 不转义\r\np= userInput // 安全\r\n\r\n// Handlebars危险模式\r\n{{{userInput}}} // 三重大括号不转义\r\n{{userInput}} // 双重大括号安全\r\n```\r\n\r\n### 6. 会话安全\r\n\r\n```javascript\r\n// 危险: 不安全的session配置\r\napp.use(session({\r\n secret: 'keyboard cat', // 弱密钥\r\n cookie: { secure: false } // 不安全\r\n}));\r\n\r\n// 安全配置\r\napp.use(session({\r\n secret: process.env.SESSION_SECRET,\r\n name: 'sessionId', // 更改默认名称\r\n cookie: {\r\n secure: true,\r\n httpOnly: true,\r\n sameSite: 'strict',\r\n maxAge: 3600000\r\n },\r\n resave: false,\r\n saveUninitialized: false\r\n}));\r\n```\r\n\r\n### 7. CORS配置\r\n\r\n```javascript\r\n// 危险: 允许所有来源\r\napp.use(cors());\r\napp.use(cors({ origin: '*' }));\r\n\r\n// 危险: 反射Origin\r\napp.use(cors({\r\n origin: (origin, callback) => {\r\n callback(null, origin); // 反射任意来源\r\n },\r\n credentials: true // 允许凭证更危险\r\n}));\r\n\r\n// 安全: 白名单\r\nconst allowedOrigins = ['https://trusted.com'];\r\napp.use(cors({\r\n origin: (origin, callback) => {\r\n if (allowedOrigins.includes(origin)) {\r\n callback(null, true);\r\n } else {\r\n callback(new Error('Not allowed'));\r\n }\r\n }\r\n}));\r\n```\r\n\r\n### 8. 依赖安全\r\n\r\n```bash\r\n# 检查已知漏洞\r\nnpm audit\r\nnpm audit --json\r\n\r\n# 检查过时依赖\r\nnpm outdated\r\n\r\n# 检查package.json中的危险包\r\n# - node-serialize (反序列化RCE)\r\n# - js-yaml < 3.13.1 (代码执行)\r\n# - lodash < 4.17.12 (原型污染)\r\n```\r\n\r\n### 9. 安全头配置\r\n\r\n```javascript\r\n// 使用helmet中间件\r\nconst helmet = require('helmet');\r\napp.use(helmet());\r\n\r\n// 或手动设置\r\napp.use((req, res, next) => {\r\n res.setHeader('X-Content-Type-Options', 'nosniff');\r\n res.setHeader('X-Frame-Options', 'DENY');\r\n res.setHeader('X-XSS-Protection', '1; mode=block');\r\n res.setHeader('Strict-Transport-Security', 'max-age=31536000');\r\n next();\r\n});\r\n```\r\n\r\n---\r\n\r\n## Express审计清单\r\n\r\n```\r\n输入验证:\r\n- [ ] 检查请求体类型验证\r\n- [ ] 搜索 req.query/req.body/req.params 使用\r\n- [ ] 验证MongoDB查询输入\r\n- [ ] 检查文件路径处理\r\n\r\n注入漏洞:\r\n- [ ] 搜索 exec/spawn 调用\r\n- [ ] 搜索 eval/Function 使用\r\n- [ ] 检查SQL/NoSQL查询构建\r\n- [ ] 搜索 _.merge/Object.assign\r\n\r\nXSS:\r\n- [ ] 检查模板引擎配置\r\n- [ ] 搜索 <%- (EJS不转义)\r\n- [ ] 搜索 {{{ (Handlebars不转义)\r\n- [ ] 检查res.send/res.json内容\r\n\r\n会话安全:\r\n- [ ] 检查session secret强度\r\n- [ ] 验证cookie安全配置\r\n- [ ] 检查JWT实现\r\n\r\n安全配置:\r\n- [ ] 检查CORS配置\r\n- [ ] 验证helmet/安全头\r\n- [ ] 检查错误处理 (不泄露栈)\r\n- [ ] 验证HTTPS强制\r\n\r\n依赖:\r\n- [ ] 运行 npm audit\r\n- [ ] 检查危险依赖包\r\n- [ ] 验证依赖版本\r\n```\r\n\r\n---\r\n\r\n## 审计正则\r\n\r\n```regex\r\n# 命令注入\r\nexec\\s*\\(|spawn\\s*\\(|child_process\r\n\r\n# NoSQL注入\r\nfindOne\\s*\\(|find\\s*\\(|updateOne\\s*\\(\r\n\r\n# 原型污染\r\n\\.merge\\s*\\(|Object\\.assign\\s*\\(|__proto__\r\n\r\n# XSS\r\n<%-|{{{\r\n\r\n# 路径遍历\r\nsendFile\\s*\\(|res\\.download\\s*\\(\r\n```\r\n\r\n## 最小 PoC 示例\r\n```bash\r\n# 路径遍历\r\ncurl \"http://localhost:3000/download?file=../../etc/passwd\"\r\n\r\n# NoSQL 注入\r\ncurl \"http://localhost:3000/user?name[$ne]=1\"\r\n\r\n# 命令注入\r\ncurl \"http://localhost:3000/ping?host=google.com;id\"\r\n```\r\n" +const SKILL_FILE_5 = "# FastAPI Security Audit\r\n\r\n> FastAPI 框架安全审计模块\r\n> 适用于: FastAPI, Starlette, SQLAlchemy\r\n\r\n## 识别特征\r\n\r\n```python\r\n# FastAPI项目识别\r\nfrom fastapi import FastAPI, Depends, HTTPException\r\napp = FastAPI()\r\n\r\n# 文件结构\r\n├── main.py\r\n├── routers/\r\n├── models/\r\n├── schemas/\r\n├── crud/\r\n└── dependencies/\r\n```\r\n\r\n---\r\n\r\n## 路由与端点分析\r\n\r\n```python\r\n# 搜索所有路由\r\n@app.get|post|put|delete|patch\r\n@router.get|post|put|delete|patch\r\n\r\n# 路径参数\r\n@app.get(\"/users/{user_id}\")\r\ndef get_user(user_id: int): # 检查IDOR\r\n\r\n# 查询参数\r\n@app.get(\"/search\")\r\ndef search(q: str = Query(...)): # 检查注入\r\n```\r\n\r\n---\r\n\r\n## FastAPI特定漏洞\r\n\r\n### 1. 依赖注入安全\r\n\r\n```python\r\n# 危险: 未验证的依赖\r\ndef get_current_user(token: str = Header(...)):\r\n # 缺少token验证\r\n return decode_token(token)\r\n\r\n# 安全: 完整验证\r\nasync def get_current_user(\r\n token: str = Depends(oauth2_scheme),\r\n db: Session = Depends(get_db)\r\n):\r\n user = authenticate_token(token, db)\r\n if not user:\r\n raise HTTPException(status_code=401)\r\n return user\r\n```\r\n\r\n### 2. Pydantic验证绕过\r\n\r\n```python\r\n# 危险: 使用dict绕过验证\r\n@app.post(\"/users\")\r\ndef create_user(data: dict): # 无schema验证\r\n db.create(User(**data))\r\n\r\n# 安全: 强类型schema\r\nclass UserCreate(BaseModel):\r\n email: EmailStr\r\n password: str = Field(..., min_length=8)\r\n\r\n@app.post(\"/users\")\r\ndef create_user(user: UserCreate):\r\n ...\r\n```\r\n\r\n### 3. 响应模型泄露\r\n\r\n```python\r\n# 危险: 返回ORM模型 (可能包含password_hash)\r\n@app.get(\"/users/{id}\")\r\ndef get_user(id: int):\r\n return db.query(User).filter(User.id == id).first()\r\n\r\n# 安全: 使用响应模型过滤\r\nclass UserResponse(BaseModel):\r\n id: int\r\n email: str\r\n # 排除敏感字段\r\n\r\n@app.get(\"/users/{id}\", response_model=UserResponse)\r\ndef get_user(id: int):\r\n return db.query(User).filter(User.id == id).first()\r\n```\r\n\r\n### 4. 背景任务注入\r\n\r\n```python\r\n# 危险: 用户输入传入背景任务\r\n@app.post(\"/process\")\r\ndef process(cmd: str, background_tasks: BackgroundTasks):\r\n background_tasks.add_task(os.system, cmd) # RCE!\r\n\r\n# 检查所有 BackgroundTasks.add_task 调用\r\n```\r\n\r\n### 5. 文件上传处理\r\n\r\n```python\r\n# 危险模式\r\n@app.post(\"/upload\")\r\nasync def upload(file: UploadFile):\r\n content = await file.read()\r\n with open(f\"/uploads/{file.filename}\", \"wb\") as f: # 路径遍历\r\n f.write(content)\r\n\r\n# 安全模式\r\nimport uuid\r\nimport os\r\n\r\n@app.post(\"/upload\")\r\nasync def upload(file: UploadFile):\r\n # 验证文件类型\r\n if file.content_type not in ALLOWED_TYPES:\r\n raise HTTPException(400)\r\n # 使用安全文件名\r\n safe_name = f\"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}\"\r\n # 限制文件大小\r\n content = await file.read(MAX_SIZE)\r\n ...\r\n```\r\n\r\n### 6. SQLAlchemy ORM注入\r\n\r\n```python\r\n# 危险: 原始SQL\r\n@app.get(\"/search\")\r\ndef search(q: str, db: Session = Depends(get_db)):\r\n return db.execute(f\"SELECT * FROM items WHERE name LIKE '%{q}%'\")\r\n\r\n# 危险: text()拼接\r\nfrom sqlalchemy import text\r\ndb.execute(text(f\"SELECT * FROM users WHERE id = {user_id}\"))\r\n\r\n# 安全: 参数化\r\ndb.execute(text(\"SELECT * FROM users WHERE id = :id\"), {\"id\": user_id})\r\n```\r\n\r\n---\r\n\r\n## FastAPI审计清单\r\n\r\n```\r\n认证与授权:\r\n- [ ] 检查OAuth2/JWT实现 (Depends)\r\n- [ ] 验证所有端点有适当的认证装饰器\r\n- [ ] 检查IDOR (路径参数未验证所有权)\r\n- [ ] 审计权限检查逻辑\r\n\r\n输入验证:\r\n- [ ] 检查是否使用Pydantic schema\r\n- [ ] 搜索 dict 类型参数 (绕过验证)\r\n- [ ] 验证Query/Path/Body参数验证\r\n- [ ] 检查文件上传处理\r\n\r\n输出安全:\r\n- [ ] 检查response_model使用\r\n- [ ] 验证敏感字段排除\r\n- [ ] 检查错误响应信息\r\n\r\n数据库:\r\n- [ ] 搜索原始SQL执行\r\n- [ ] 检查SQLAlchemy text()使用\r\n- [ ] 验证ORM查询安全\r\n\r\nCORS:\r\n- [ ] 检查CORSMiddleware配置\r\n- [ ] 验证allow_origins设置\r\n- [ ] 检查allow_credentials=True风险\r\n```\r\n\r\n---\r\n\r\n## 审计正则\r\n\r\n```regex\r\n# 路由搜索\r\n@(app|router)\\.(get|post|put|delete|patch)\\s*\\(\r\n\r\n# Pydantic绕过\r\ndef\\s+\\w+\\([^)]*:\\s*dict[^)]*\\)\r\n\r\n# 背景任务\r\nBackgroundTasks\\.add_task\r\n\r\n# SQL注入\r\ndb\\.execute\\s*\\(f['\"']|text\\s*\\(f['\"']\r\n\r\n# 文件上传\r\nUploadFile.*file\\.filename\r\n```\r\n\r\n## 最小 PoC 示例\r\n```bash\r\n# SQL 注入\r\ncurl \"http://localhost:8000/users?q=1' OR '1'='1\"\r\n\r\n# SSRF\r\ncurl \"http://localhost:8000/fetch?url=http://169.254.169.254/latest/meta-data/\"\r\n\r\n# 路径遍历上传文件名\r\ncurl -F \"file=@/etc/passwd;filename=../../etc/passwd\" http://localhost:8000/upload\r\n```\r\n" +const SKILL_FILE_6 = "# Flask Security Audit\r\n\r\n> Flask 框架安全审计模块\r\n> 适用于: Flask, Flask-RESTful, Flask-Login, Flask-JWT-Extended\r\n\r\n---\r\n\r\n## 识别特征\r\n\r\n```python\r\n# Flask 项目识别\r\nfrom flask import Flask, request, render_template\r\napp = Flask(__name__)\r\n\r\n# 常见目录结构\r\n├── app/\r\n│ ├── __init__.py # create_app 工厂\r\n│ ├── routes/ # 路由蓝图\r\n│ ├── models/ # SQLAlchemy 模型\r\n│ ├── templates/ # Jinja2 模板\r\n│ └── static/ # 静态资源\r\n├── config.py # 配置\r\n├── requirements.txt\r\n└── run.py / wsgi.py\r\n```\r\n\r\n---\r\n\r\n## Critical 漏洞\r\n\r\n### 1. 调试模式 RCE\r\n\r\n```python\r\n# 危险: 生产环境开启调试\r\nif __name__ == '__main__':\r\n app.run(debug=True) # Werkzeug debugger PIN 可被计算\r\n\r\n# 危险: 配置文件\r\nDEBUG = True\r\nFLASK_DEBUG = 1\r\n\r\n# PIN 计算要素 (可用于爆破):\r\n# - username (运行用户)\r\n# - modname (通常 flask.app)\r\n# - getattr(app, '__name__', app.__class__.__name__)\r\n# - getattr(mod, '__file__', None) → app.py 路径\r\n# - uuid.getnode() → MAC 地址\r\n# - get_machine_id() → /etc/machine-id 或 /proc/sys/kernel/random/boot_id\r\n\r\n# 检测\r\ngrep -rn \"debug\\s*=\\s*True\" --include=\"*.py\"\r\ngrep -rn \"FLASK_DEBUG\\|DEBUG\\s*=\" --include=\"*.py\" --include=\"*.env\"\r\n```\r\n\r\n### 2. SECRET_KEY 泄露\r\n\r\n```python\r\n# 危险: 硬编码密钥\r\napp.secret_key = 'super_secret_key'\r\napp.config['SECRET_KEY'] = 'hardcoded'\r\n\r\n# 危险: 弱密钥\r\nSECRET_KEY = 'development'\r\nSECRET_KEY = os.urandom(16) # 每次重启变化,session 失效\r\n\r\n# 影响:\r\n# - Session 伪造\r\n# - Flask-Login 令牌伪造\r\n# - CSRF token 伪造\r\n# - itsdangerous 签名伪造\r\n\r\n# 安全: 从环境变量读取\r\napp.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')\r\nif not app.config['SECRET_KEY']:\r\n raise ValueError(\"SECRET_KEY not set\")\r\n\r\n# 检测\r\ngrep -rn \"secret_key\\s*=\\|SECRET_KEY\\s*=\" --include=\"*.py\"\r\n```\r\n\r\n### 3. SSTI (Server-Side Template Injection)\r\n\r\n```python\r\n# 危险: render_template_string 用户输入\r\nfrom flask import render_template_string\r\n\r\n@app.route('/hello')\r\ndef hello():\r\n name = request.args.get('name', 'World')\r\n template = f'

Hello {name}!

'\r\n return render_template_string(template)\r\n\r\n# payload: {{config.items()}}\r\n# payload: {{''.__class__.__mro__[1].__subclasses__()}}\r\n# RCE payload:\r\n# {{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('id').read()}}\r\n\r\n# 危险: 用户控制模板名\r\n@app.route('/page/