xr843/llm-seclint
GitHub: xr843/llm-seclint
一款面向 Python LLM 应用的静态安全代码扫描工具,在发布前通过 AST 解析与污点分析发现 LLM 特有的安全漏洞。
Stars: 2 | Forks: 0
# llm-seclint
[](https://github.com/xr843/llm-seclint/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
[](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嵌入, 逆向工具, 错误基检测, 静态代码分析