xr843/llm-seclint

GitHub: xr843/llm-seclint

一款面向 Python LLM 应用的静态安全代码扫描工具,在发布前通过 AST 解析与污点分析发现 LLM 特有的安全漏洞。

Stars: 2 | Forks: 0

# llm-seclint [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/xr843/llm-seclint/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) **在 LLM 应用发布前发现其安全漏洞。** llm-seclint 是一款静态分析工具,用于扫描 Python 源代码,查找 LLM 应用特有的安全问题。可以把它看作是 AI 时代的 [Bandit](https://github.com/PyCQA/bandit)。 ## 问题所在 LLM 应用引入了一类传统安全工具容易遗漏的新型漏洞: - 通过未过滤的用户输入导致的 **提示词注入(Prompt injection)** - 当 LLM 输出流入 `eval()`、`subprocess` 或 SQL 查询时导致的 **任意代码执行** - 通过硬编码凭证导致的 **API key 泄露** - 当 LLM 输出控制文件访问时导致的 **路径遍历** - 当动态内容未沙箱化即进入模板引擎时导致的 **模板注入** - 在无保护情况下解析不受信任的 XML 时导致的 **XML 外部实体(XXE)** - 通过未锁定版本的 LLM 依赖项导致的 **供应链攻击** 像 [garak](https://github.com/leondz/garak)、[LLM Guard](https://github.com/protectai/llm-guard) 和 [Guardrails](https://github.com/guardrails-ai/guardrails) 这样的现有工具运行在 **runtime** —— 它们测试已部署的模型或过滤实时流量。它们都不会在你发布代码前分析你的 **源代码**。 **llm-seclint** 填补了这一空白。它使用 AST 分析来扫描你的 Python 源代码,在开发阶段发现 LLM 特有的安全问题,就像 Bandit 处理通用 Python 安全问题那样。 ## 快速开始 从源码安装(计划发布 PyPI 包,但尚未发布): ``` pip install git+https://github.com/xr843/llm-seclint.git ``` 扫描你的项目: ``` llm-seclint scan . ``` 就这样。你会看到类似如下的输出: ``` src/app.py !! L12 [LS001] Hardcoded API key assigned to 'OPENAI_API_KEY' ! L41 [LS006] Dynamic input passed to eval() !! L55 [LS004] Dynamic output passed to subprocess.run() with shell=True Found 3 issue(s): 2 critical, 1 high Scanned in 0.03s ``` 默认情况下,只会运行 **stable**(低误报)规则。添加 `--experimental` 也会启用启发式规则 LS002 —— 参见 [规则稳定性](#rule-stability)。 ## 工作原理 ``` Source Code → AST Parsing → 9 Security Rules → Findings Report ├─ LS001: Hardcoded API Keys ├─ LS002: Prompt Injection ├─ LS003: SQL Injection via LLM ├─ LS004: Shell Injection via LLM ├─ LS005: Path Traversal via LLM ├─ LS006: Insecure Deserialization ├─ LS007: Template Injection (SSTI) ├─ LS008: XXE XML Parsing └─ LS010: Unpinned LLM Dependencies ``` llm-seclint 会将你的 Python 文件解析为抽象语法树,并应用专门针对 LLM 特有数据流的安全规则。无需模型访问,也没有 runtime 开销 —— 只提供快速、确定性的分析。 ### 已确认的数据流(污点分析) 除了匹配危险的 sink 之外,llm-seclint 还运行了一个轻量级的**过程内污点分析引擎**,用于追踪不受信任的值 —— 包括 **LLM 输出**(`openai`/`litellm`/Anthropic completions)和 **用户输入**(`input()`、`sys.argv`、Flask `request.*`) —— 在函数内的赋值、属性/下标链以及字符串构建过程中的流动。当这样的值到达 sink 时,发现结果会被标记为 **`confirmed LLM→sink`** / **`USER→sink dataflow`**,并携带结构化的 `taint_source` 字段 —— 这样你就可以区分一个真实的“不受信任输入流入 `eval()`”的过程与一个仅仅是动态参数的过程: ``` resp = openai.chat.completions.create(model="gpt-4", messages=msgs) code = resp.choices[0].message.content eval(code) # LS006 — confirmed LLM→sink dataflow ``` 如今,这种确认机制驱动着每一个 sink 规则 —— **LS004** (shell)、**LS005** (path)、**LS006** (`eval`/`exec`/`pickle`)、**LS007** (SSTI) 和 **LS008** (XXE);它从不抑制或降级现有的发现结果(仅仅是动态参数也会被报告)。LS003 (SQL) 借此升级为 stable,实验性的 **LS002**(提示词注入)也会为其经过污点确认的子集进行标注。其范围被刻意限制在:单函数、单次遍历(暂无跨函数或控制流图精度)。 ## 这些规则的来源 每一条规则都对应着我在**手动审计**流行的开源 LLM 项目时发现的真实不安全模式。正是这些审计促使我构建了 llm-seclint —— 以便在这些问题发布之前,自动捕获同类问题。我向上游提出了修复建议;有些建议由于维护者倾向于使用自己的方案而被拒绝,但这些底层的模式正是如今规则所检测到的内容。 | 项目 | 发现的模式 | 规则 | 上游 PR | |---------|---------------|------|-------------| | [Dify](https://github.com/langgenius/dify) (100k★) | 对数据库数据进行不安全的 `pickle.loads()` | LS006 | 审计中发现 | | [Dify](https://github.com/langgenius/dify) (100k★) | 在 UNSAFE 模式下使用 `render_template_string()` 导致的 SSTI | LS007 | 审计中发现 | | [Dify](https://github.com/langgenius/dify) (100k★) | 在 VDB 驱动程序中进行 SQL f-string 插值 | LS003 | 审计中发现 | | [LiteLLM](https://github.com/BerriAI/litellm) (20k★) | 自定义代码防护机制中的 `exec()` | LS006 | [#24455](https://github.com/BerriAI/litellm/pull/24455) (已关闭) | | [LiteLLM](https://github.com/BerriAI/litellm) (20k★) | 提示词管理器中的 Jinja2 SSTI | LS007 | [#24458](https://github.com/BerriAI/litellm/pull/24458) (已关闭) | | [vllm](https://github.com/vllm-project/vllm) (45k★) | 示例代码中对 LLM 输出使用 `eval()` | LS006 | [#37939](https://github.com/vllm-project/vllm/pull/37939) (已关闭) | | [crewAI](https://github.com/crewAIInc/crewAI) (30k★) | XML 解析中的 XXE(使用 `defusedxml`) | LS008 | [#5005](https://github.com/crewAIInc/crewAI/pull/5005) (已关闭) | ## 检测内容 | 规则 | 名称 | 严重程度 | 稳定性 | 描述 | |------|------|----------|-----------|-------------| | LS001 | `hardcoded-api-key` | CRITICAL | stable | 硬编码的 LLM 提供商(OpenAI、Anthropic、xAI 等)API key | | LS002 | `prompt-concat-injection` | HIGH | experimental | 通过 f-string、`+` 或 `.format()` 将用户输入拼接到 LLM 提示词中 | | LS003 | `llm-to-sql-injection` | CRITICAL | stable | LLM/用户输入被插值到 SQL 查询中(经污点分析确认) | | LS004 | `llm-to-shell-injection` | CRITICAL | stable | LLM 输出被传递给 `subprocess` / `os.system` | | LS005 | `llm-to-path-traversal` | HIGH | stable | LLM 输出被用作文件路径 | | LS006 | `insecure-deserialization` | HIGH | stable | 在动态输入上使用 `eval` / `exec` / `pickle` / 不安全的 YAML | | LS007 | `server-side-template-injection` | CRITICAL | stable | 动态内容在未沙箱化的情况下被传递给模板引擎 | | LS008 | `xxe-xml-parsing` | HIGH | stable | 未加保护地解析 XML,容易受到外部实体攻击 | | LS010 | `unpinned-llm-dependency` | HIGH | stable | LLM 依赖项使用了未锁定的版本约束(例如只有 `>=` 而没有 `<`),容易受到供应链攻击 | ### 规则稳定性 根据规则区分真实问题和噪音的可靠程度,对它们进行了分级: - **stable** —— sink 驱动、模式独特或**经污点分析确认**(硬编码的提供商 key、对动态输入使用 `eval()`、容易引发 XXE 的解析,或者[污点引擎](#confirmed-dataflow-taint-analysis)追踪到的从 LLM/用户输入进入 SQL 的值)。误报率低。**默认开启。** - **experimental** —— 依赖于命名/关键字启发式方法来*猜测*数据是来自 LLM 还是用户(LS002 会根据类似 "you are" 这样的词汇进行判断)。误报率较高,因此**除非你传入 `--experimental` 否则处于关闭状态**。 这使得默认扫描保持高信噪比。运行 `llm-seclint rules` 可以查看每条规则的稳定性。一旦污点引擎能够确认数据源,LS003 就升级为了 stable。LS002 则被刻意保留为 experimental:提示词注入的主要形式是流入提示词的函数*参数*,而过程内污点分析无法确认这一点 —— 因此宽泛的启发式方法发挥了作用(并且当你运行 `--experimental` 时,可以通过污点确认的子集会被标注出来)。 ### 示例 #### LS001:硬编码 API Key ``` # Bad - 被 llm-seclint 检测到 openai.api_key = "sk-proj-abc123..." client = Anthropic(api_key="sk-ant-api03-...") # Good openai.api_key = os.environ["OPENAI_API_KEY"] client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) ``` #### LS002:提示词注入 ``` # Bad - 用户输入直接位于 prompt 中 prompt = f"You are a bot. User says: {user_input}" # Good - 分离 message roles messages = [ {"role": "system", "content": "You are a bot."}, {"role": "user", "content": user_input}, ] ``` #### LS003:通过 LLM 输出进行的 SQL 注入 ``` # Bad - LLM 输出至 SQL cursor.execute(f"SELECT * FROM users WHERE name = '{llm_response}'") # Good - parameterized query cursor.execute("SELECT * FROM users WHERE name = ?", (llm_response,)) ``` #### LS004:通过 LLM 输出进行的 Shell 注入 ``` # Bad - LLM 输出至 shell subprocess.run(llm_output, shell=True) # Good - 针对 allowlist 进行验证 if command in ALLOWED_COMMANDS: subprocess.run([command], check=False) ``` #### LS005:通过 LLM 输出进行的路径遍历 ``` # Bad - LLM 输出作为文件路径 with open(llm_response) as f: ... # Good - 针对 base directory 进行验证 path = (ALLOWED_BASE / filename).resolve() assert str(path).startswith(str(ALLOWED_BASE)) ``` #### LS006:不安全的反序列化 ``` # Bad - 对 LLM 响应执行 eval data = eval(llm_response) # Good - 使用安全解析 data = json.loads(llm_response) ``` #### LS007:服务端模板注入 ``` # Bad - 用户输入位于 template string 中 render_template_string(f"

Hello {user_input}

") # Good - 通过 context 传递变量 render_template_string("

Hello {{ name }}

", name=user_input) ``` #### LS008:XXE XML 解析 ``` # Bad - 在未受保护的情况下解析不受信任的 XML tree = etree.parse(user_uploaded_file) # Good - 使用 defusedxml from defusedxml.lxml import parse tree = parse(user_uploaded_file) ``` #### LS010:未锁定的 LLM 依赖 ``` # Bad - 开放式约束允许恶意的未来版本 (requirements.txt) litellm>=1.64.0 dspy>=2.0 openai>=1.0 # Good - 锁定到确切版本 litellm==1.82.2 # Good - 上限防止自动升级到受损版本 litellm>=1.64.0,<1.83 ``` 这条规则的动机来源于 [litellm 供应链攻击](https://blog.pypi.org/posts/2025-01-14-litellm-typosquat/),其中 `dspy` 使用了 `litellm>=1.64.0`,导致一个被攻破的版本被自动拉取。它会扫描 `requirements.txt`、`pyproject.toml` 和 `setup.cfg`,查找带有开放式 `>=` 约束的 LLM 包。 ## 框架支持 llm-seclint 能够理解来自流行 LLM 框架的模式: - **LangChain** —— `PromptTemplate`、`ChatPromptTemplate.from_messages()`、`HumanMessagePromptTemplate` - **LiteLLM** —— `litellm.completion()`、`litellm.acompletion()` - **OpenAI SDK** —— `openai.ChatCompletion.create()`、`client.chat.completions.create()` - **Anthropic SDK** —— `anthropic.Anthropic().messages.create()` - **Flask/Jinja2** —— `render_template_string()`、`jinja2.Template()` ## OWASP LLM Top 10 映射 | OWASP LLM Top 10 | llm-seclint 规则 | |---|---| | LLM01:提示词注入 | LS002 | | LLM02:不安全的输出处理 | LS003, LS004, LS005, LS006, LS007 | | LLM06:敏感信息泄露 | LS001 | | A05:2021:安全配置错误 | LS008 (CWE-611) | ## 对比 | 特性 | llm-seclint | garak | LLM Guard | Guardrails | |---------|:-----------:|:-----:|:---------:|:----------:| | 分析类型 | 静态 (AST) | 动态(探测) | Runtime(过滤) | Runtime(防护) | | 需要运行模型 | 否 | 是 | 是 | 是 | | CI/CD 集成 | 原生支持 | 手动 | 手动 | 手动 | | SARIF 输出 | 是 | 否 | 否 | 否 | | `# nosec` 内联抑制 | 是 | N/A | N/A | N/A | | Pre-commit hook | 是 | 否 | 否 | 否 | | 查找硬编码的 key | 是 | 否 | 否 | 否 | | 查找提示词注入模式 | 是 | 测试 | 过滤 | 过滤 | | 查找输出处理缺陷 | 是 | 否 | 否 | 否 | | 语言 | Python | Python | Python | Python | ## CLI 用法 ``` # 扫描当前目录 llm-seclint scan . # 扫描特定文件 llm-seclint scan src/ --include "*.py" # JSON 输出 llm-seclint scan . --format json -o results.json # SARIF 输出(用于 GitHub Code Scanning) llm-seclint scan . --format sarif -o results.sarif # 忽略特定规则 llm-seclint scan . --ignore LS001,LS002 # 设置最低严重性 llm-seclint scan . --min-severity HIGH # 列出所有规则 llm-seclint rules # 显示版本 llm-seclint --version ``` ## 扫描配置 llm-seclint 提供了两种扫描配置: - `--profile app`(默认) —— 针对 LLM 应用的全面扫描 - `--profile engine` —— 针对 LLM 推理引擎(vllm、TGI 等)进行调整。 由于处理提示词是引擎的工作,因此会禁用 LS002(提示词注入)。 ### 实验性规则 默认情况下只会运行 [stable](#rule-stability) 规则。添加 `--experimental` 也会 启用启发式规则 LS002,或者在 你的配置文件中设置 `include_experimental: true`: ``` llm-seclint scan . --experimental ``` ### 内联抑制 使用 `# nosec` 注释抑制特定的发现结果: ``` api_key = "sk-test-key-for-ci" # nosec LS001 ``` ## GitHub Code Scanning 集成 llm-seclint 支持 [SARIF](https://sarifweb.azurewebsites.net/) 输出,以便直接与 GitHub Code Scanning 集成。将以下内容添加到你的 GitHub Actions 工作流中: ``` - name: Run llm-seclint run: llm-seclint scan . --format sarif -o results.sarif - name: Upload SARIF to GitHub uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif ``` ## Pre-commit 将 llm-seclint 添加到你的 `.pre-commit-config.yaml` 中: ``` repos: - repo: https://github.com/xr843/llm-seclint rev: v0.1.0 hooks: - id: llm-seclint ``` ## 配置 在你的项目根目录下创建一个 `.llm-seclint.yml`: ``` # 要包含的文件模式 include_patterns: - "*.py" # 要排除的文件模式 exclude_patterns: - "test_*.py" - "*_test.py" # 要忽略的规则 ignore_rules: - LS005 # 要报告的最低严重性 (CRITICAL, HIGH, MEDIUM, LOW, INFO) min_severity: MEDIUM # 同时运行 experimental(启发式,higher false-positive)规则 include_experimental: false ``` ## 开发安装 ``` git clone https://github.com/xr843/llm-seclint.git cd llm-seclint pip install -e ".[dev]" pytest ``` ## 路线图 - **v0.2** *(已发布)*:过程内污点追踪(真实的 LLM/用户 → sink 数据流) —— 每个 sink 规则都会确认数据流,并且 LS003 升级为 stable - **下一步**:跨函数/控制流图污点精度,使得通过 函数参数的流程得以确认(这也会让 LS002 升级为 stable) - **v0.3**:JavaScript/TypeScript 分析器(LangChain.js、Vercel AI SDK) - **v0.4**:特定框架的规则(LangChain、LlamaIndex、Semantic Kernel) - **v0.5**:通过 `--fix` 标志提供自动修复建议 - **v1.0**:稳定的 API、VS Code 扩展 ## 许可证 MIT
标签:AI安全, Chat Copilot, CISA项目, DLL 劫持, DOE合作, HTTP工具, Python, SAST, StruQ, URL发现, 大语言模型, 安全专业人员, 安全检测, 无后门, 盲注攻击, 自动化payload嵌入, 逆向工具, 错误基检测, 静态代码分析