{"id":51573744,"url":"https://github.com/xr843/llm-seclint","last_synced_at":"2026-07-10T22:02:17.139Z","repository":{"id":367714367,"uuid":"1190633327","full_name":"xr843/llm-seclint","owner":"xr843","description":"Static security linter for LLM-powered applications. The Bandit for the AI era.","archived":false,"fork":false,"pushed_at":"2026-06-27T09:59:50.000Z","size":245,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-27T10:06:29.129Z","etag":null,"topics":["ai-security","linter","llm","owasp","prompt-injection","python","sast","security","static-analysis","vulnerability-scanner"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/llm-seclint/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xr843.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-24T13:26:00.000Z","updated_at":"2026-06-27T09:58:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/xr843/llm-seclint","commit_stats":null,"previous_names":["xr843/llm-seclint"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/xr843/llm-seclint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xr843%2Fllm-seclint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xr843%2Fllm-seclint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xr843%2Fllm-seclint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xr843%2Fllm-seclint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xr843","download_url":"https://codeload.github.com/xr843/llm-seclint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xr843%2Fllm-seclint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35344523,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-security","linter","llm","owasp","prompt-injection","python","sast","security","static-analysis","vulnerability-scanner"],"created_at":"2026-07-10T22:02:15.807Z","updated_at":"2026-07-10T22:02:17.122Z","avatar_url":"https://github.com/xr843.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# llm-seclint\n\n[![CI](https://github.com/xr843/llm-seclint/actions/workflows/ci.yml/badge.svg)](https://github.com/xr843/llm-seclint/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)\n\n\u003e **Status: pre-release (v0.1.0, alpha).** Not yet published to PyPI — install from source (see [Quick Start](#quick-start)). The API and rule IDs may change before v1.0.\n\n**Find LLM security vulnerabilities before they ship.**\n\nllm-seclint is a static analysis tool that scans Python source code for security issues specific to LLM-powered applications. Think [Bandit](https://github.com/PyCQA/bandit), but for the AI era.\n\n## The Problem\n\nLLM-powered applications introduce a new class of vulnerabilities that traditional security tools miss:\n\n- **Prompt injection** through unsanitized user input\n- **Arbitrary code execution** when LLM output flows into `eval()`, `subprocess`, or SQL queries\n- **API key leakage** through hardcoded credentials\n- **Path traversal** when LLM output controls file access\n- **Template injection** when dynamic content reaches template engines unsandboxed\n- **XML external entities** when parsing untrusted XML without protection\n- **Supply chain attacks** through unpinned LLM dependency versions\n\nExisting tools like [garak](https://github.com/leondz/garak), [LLM Guard](https://github.com/protectai/llm-guard), and [Guardrails](https://github.com/guardrails-ai/guardrails) operate at **runtime** -- they test deployed models or filter live traffic. None of them analyze your **source code** before you ship.\n\n**llm-seclint** fills this gap. It scans your Python source using AST analysis to find LLM-specific security issues at development time, just like Bandit does for general Python security.\n\n## Quick Start\n\nInstall from source (a PyPI package is planned but not yet published):\n\n```bash\npip install git+https://github.com/xr843/llm-seclint.git\n```\n\nScan your project:\n\n```bash\nllm-seclint scan .\n```\n\nThat's it. You'll see output like:\n\n```text\nsrc/app.py\n  !! L12 [LS001] Hardcoded API key assigned to 'OPENAI_API_KEY'\n  !  L41 [LS006] Dynamic input passed to eval()\n  !! L55 [LS004] Dynamic output passed to subprocess.run() with shell=True\n\nFound 3 issue(s): 2 critical, 1 high\nScanned in 0.03s\n```\n\nBy default only **stable** (low false-positive) rules run. Add `--experimental`\nto also enable the heuristic rule LS002 — see [Rule stability](#rule-stability).\n\n## How It Works\n\n```text\nSource Code → AST Parsing → 10 Security Rules → Findings Report\n                              ├─ LS001: Hardcoded API Keys\n                              ├─ LS002: Prompt Injection\n                              ├─ LS003: SQL Injection via LLM\n                              ├─ LS004: Shell Injection via LLM\n                              ├─ LS005: Path Traversal via LLM\n                              ├─ LS006: Insecure Deserialization\n                              ├─ LS007: Template Injection (SSTI)\n                              ├─ LS008: XXE XML Parsing\n                              ├─ LS009: SSRF via LLM/user output\n                              └─ LS010: Unpinned LLM Dependencies\n```\n\nllm-seclint parses your Python files into Abstract Syntax Trees and applies targeted security rules that understand LLM-specific data flows. No model access required, no runtime overhead -- just fast, deterministic analysis.\n\n### Confirmed dataflow (taint analysis)\n\nBeyond matching dangerous sinks, llm-seclint runs a lightweight **intra-procedural\ntaint engine** that tracks untrusted values — both **LLM output** (`openai`/\n`litellm`/Anthropic completions) and **user input** (`input()`, `sys.argv`, Flask\n`request.*`) — as they flow through assignments, attribute/subscript chains, and\nstring building within a function. When such a value reaches a sink, the finding\nis annotated **`confirmed LLM→sink`** / **`USER→sink dataflow`** and carries a\nstructured `taint_source` field — so you can tell a real untrusted-input-to-\n`eval()` flow from a merely-dynamic argument:\n\n```python\nresp = openai.chat.completions.create(model=\"gpt-4\", messages=msgs)\ncode = resp.choices[0].message.content\neval(code)   # LS006 — confirmed LLM→sink dataflow\n```\n\nToday this confirmation drives every sink rule — **LS004** (shell), **LS005**\n(path), **LS006** (`eval`/`exec`/`pickle`), **LS007** (SSTI) and **LS008** (XXE);\nit never suppresses or downgrades an existing finding (a merely-dynamic argument\nis still reported). LS003 (SQL) graduated to stable on the back of this, and the\nexperimental **LS002** (prompt injection) annotates its taint-confirmed subset\ntoo. Scope is\ndeliberately bounded: single-function, single-pass (no cross-function or\ncontrol-flow-graph precision yet).\n\n## Where These Rules Come From\n\nEvery rule maps to a real insecure pattern I found while **manually auditing**\npopular open-source LLM projects. Those audits are what motivated building\nllm-seclint — to catch the same classes of issue automatically, before they\nship. Fixes were proposed upstream; several were declined in favor of the\nmaintainers' own approaches, but the underlying patterns are exactly what the\nrules now detect.\n\n| Project | Pattern found | Rule | Upstream PR |\n|---------|---------------|------|-------------|\n| [Dify](https://github.com/langgenius/dify) (100k★) | Unsafe `pickle.loads()` on database data | LS006 | found in audit |\n| [Dify](https://github.com/langgenius/dify) (100k★) | `render_template_string()` SSTI in UNSAFE mode | LS007 | found in audit |\n| [Dify](https://github.com/langgenius/dify) (100k★) | SQL f-string interpolation in VDB drivers | LS003 | found in audit |\n| [LiteLLM](https://github.com/BerriAI/litellm) (20k★) | `exec()` in custom code guardrails | LS006 | [#24455](https://github.com/BerriAI/litellm/pull/24455) (closed) |\n| [LiteLLM](https://github.com/BerriAI/litellm) (20k★) | Jinja2 SSTI in prompt managers | LS007 | [#24458](https://github.com/BerriAI/litellm/pull/24458) (closed) |\n| [vllm](https://github.com/vllm-project/vllm) (45k★) | `eval()` on LLM output in example code | LS006 | [#37939](https://github.com/vllm-project/vllm/pull/37939) (closed) |\n| [crewAI](https://github.com/crewAIInc/crewAI) (30k★) | XXE in XML parsing (use `defusedxml`) | LS008 | [#5005](https://github.com/crewAIInc/crewAI/pull/5005) (closed) |\n\n## What It Detects\n\n| Rule | Name | Severity | Stability | Description |\n|------|------|----------|-----------|-------------|\n| LS001 | `hardcoded-api-key` | CRITICAL | stable | Hardcoded API keys for LLM providers (OpenAI, Anthropic, xAI, etc.) |\n| LS002 | `prompt-concat-injection` | HIGH | experimental | User input concatenated into LLM prompts via f-strings, `+`, or `.format()` |\n| LS003 | `llm-to-sql-injection` | CRITICAL | stable | LLM/user input interpolated into SQL queries (taint-confirmed) |\n| LS004 | `llm-to-shell-injection` | CRITICAL | stable | LLM output passed to `subprocess` / `os.system` |\n| LS005 | `llm-to-path-traversal` | HIGH | stable | LLM output used as file paths |\n| LS006 | `insecure-deserialization` | HIGH | stable | `eval` / `exec` / `pickle` / unsafe YAML on dynamic input |\n| LS007 | `server-side-template-injection` | CRITICAL | stable | Dynamic content passed to template engine without sandboxing |\n| LS008 | `xxe-xml-parsing` | HIGH | stable | XML parsing without protection against external entity attacks |\n| LS009 | `llm-to-ssrf` | HIGH | stable | LLM/user input used as an outbound request URL (taint-confirmed SSRF) |\n| LS010 | `unpinned-llm-dependency` | HIGH | stable | LLM dependency uses unpinned version constraint (e.g. `\u003e=` without `\u003c`), vulnerable to supply chain attacks |\n\n### Rule stability\n\nRules are graded by how reliably they distinguish a real issue from noise:\n\n- **stable** — sink-driven, pattern-unique, or **taint-confirmed** (a hardcoded\n  provider key, `eval()` on dynamic input, XXE-prone parsing, or a value the\n  [taint engine](#confirmed-dataflow-taint-analysis) traces from LLM/user input\n  into SQL). Low false-positive. **On by default.**\n- **experimental** — relies on naming/keyword heuristics to *guess* whether data\n  is LLM- or user-derived (LS002 keys off words like \"you are\"). Higher\n  false-positive, so **off unless you pass `--experimental`**.\n\nThis keeps the default scan high-signal. Run `llm-seclint rules` to see each\nrule's stability. LS003 graduated to stable once the taint engine could confirm\nthe data source. LS002 stays experimental on purpose: prompt injection's\ndominant shape is a function *parameter* flowing into a prompt, which\nintra-procedural taint cannot confirm — so the broad heuristic earns its keep\n(and the taint-confirmable subset is annotated when you run `--experimental`).\n\n### Examples\n\n#### LS001: Hardcoded API Key\n\n```python\n# Bad - detected by llm-seclint\nopenai.api_key = \"sk-proj-abc123...\"\nclient = Anthropic(api_key=\"sk-ant-api03-...\")\n\n# Good\nopenai.api_key = os.environ[\"OPENAI_API_KEY\"]\nclient = Anthropic(api_key=os.environ[\"ANTHROPIC_API_KEY\"])\n```\n\n#### LS002: Prompt Injection\n\n```python\n# Bad - user input directly in prompt\nprompt = f\"You are a bot. User says: {user_input}\"\n\n# Good - separate message roles\nmessages = [\n    {\"role\": \"system\", \"content\": \"You are a bot.\"},\n    {\"role\": \"user\", \"content\": user_input},\n]\n```\n\n#### LS003: SQL Injection via LLM Output\n\n```python\n# Bad - LLM output in SQL\ncursor.execute(f\"SELECT * FROM users WHERE name = '{llm_response}'\")\n\n# Good - parameterized query\ncursor.execute(\"SELECT * FROM users WHERE name = ?\", (llm_response,))\n```\n\n#### LS004: Shell Injection via LLM Output\n\n```python\n# Bad - LLM output to shell\nsubprocess.run(llm_output, shell=True)\n\n# Good - validate against allowlist\nif command in ALLOWED_COMMANDS:\n    subprocess.run([command], check=False)\n```\n\n#### LS005: Path Traversal via LLM Output\n\n```python\n# Bad - LLM output as file path\nwith open(llm_response) as f: ...\n\n# Good - validate against base directory\npath = (ALLOWED_BASE / filename).resolve()\nassert str(path).startswith(str(ALLOWED_BASE))\n```\n\n#### LS006: Insecure Deserialization\n\n```python\n# Bad - eval on LLM response\ndata = eval(llm_response)\n\n# Good - use safe parsing\ndata = json.loads(llm_response)\n```\n\n#### LS007: Server-Side Template Injection\n\n```python\n# Bad - user input in template string\nrender_template_string(f\"\u003ch1\u003eHello {user_input}\u003c/h1\u003e\")\n\n# Good - pass variables through context\nrender_template_string(\"\u003ch1\u003eHello {{ name }}\u003c/h1\u003e\", name=user_input)\n```\n\n#### LS008: XXE XML Parsing\n\n```python\n# Bad - parsing untrusted XML without protection\ntree = etree.parse(user_uploaded_file)\n\n# Good - use defusedxml\nfrom defusedxml.lxml import parse\ntree = parse(user_uploaded_file)\n```\n\n#### LS010: Unpinned LLM Dependency\n\n```text\n# Bad - open-ended constraint allows malicious future releases (requirements.txt)\nlitellm\u003e=1.64.0\ndspy\u003e=2.0\nopenai\u003e=1.0\n\n# Good - pinned to exact version\nlitellm==1.82.2\n\n# Good - upper bound prevents auto-upgrade to compromised versions\nlitellm\u003e=1.64.0,\u003c1.83\n```\n\nThis rule was motivated by the [litellm supply chain attack](https://blog.pypi.org/posts/2025-01-14-litellm-typosquat/) where `dspy` used `litellm\u003e=1.64.0` and a compromised release was automatically pulled in. It scans `requirements.txt`, `pyproject.toml`, and `setup.cfg` for LLM packages with open-ended `\u003e=` constraints.\n\n## Framework Support\n\nllm-seclint understands patterns from popular LLM frameworks:\n\n- **LangChain** -- `PromptTemplate`, `ChatPromptTemplate.from_messages()`, `HumanMessagePromptTemplate`\n- **LiteLLM** -- `litellm.completion()`, `litellm.acompletion()`\n- **OpenAI SDK** -- `openai.ChatCompletion.create()`, `client.chat.completions.create()`\n- **Anthropic SDK** -- `anthropic.Anthropic().messages.create()`\n- **Flask/Jinja2** -- `render_template_string()`, `jinja2.Template()`\n\n## OWASP LLM Top 10 Mapping\n\n| OWASP LLM Top 10 | llm-seclint Rules |\n|---|---|\n| LLM01: Prompt Injection | LS002 |\n| LLM02: Insecure Output Handling | LS003, LS004, LS005, LS006, LS007, LS009 |\n| LLM06: Sensitive Information Disclosure | LS001 |\n| A05:2021: Security Misconfiguration | LS008 (CWE-611) |\n\n## Comparison\n\n| Feature | llm-seclint | garak | LLM Guard | Guardrails |\n|---------|:-----------:|:-----:|:---------:|:----------:|\n| Analysis type | Static (AST) | Dynamic (probing) | Runtime (filter) | Runtime (guard) |\n| Requires running model | No | Yes | Yes | Yes |\n| CI/CD integration | Native | Manual | Manual | Manual |\n| SARIF output | Yes | No | No | No |\n| `# nosec` inline suppression | Yes | N/A | N/A | N/A |\n| Pre-commit hook | Yes | No | No | No |\n| Finds hardcoded keys | Yes | No | No | No |\n| Finds prompt injection patterns | Yes | Tests for | Filters | Filters |\n| Finds output handling flaws | Yes | No | No | No |\n| Language | Python | Python | Python | Python |\n\n## CLI Usage\n\n```bash\n# Scan current directory\nllm-seclint scan .\n\n# Scan specific files\nllm-seclint scan src/ --include \"*.py\"\n\n# JSON output\nllm-seclint scan . --format json -o results.json\n\n# SARIF output (for GitHub Code Scanning)\nllm-seclint scan . --format sarif -o results.sarif\n\n# Ignore specific rules\nllm-seclint scan . --ignore LS001,LS002\n\n# Set minimum severity\nllm-seclint scan . --min-severity HIGH\n\n# List all rules\nllm-seclint rules\n\n# Show version\nllm-seclint --version\n```\n\n## Profiles\n\nllm-seclint ships with two scan profiles:\n\n- `--profile app` (default) — Full scan for LLM-powered applications\n- `--profile engine` — Tuned for LLM inference engines (vllm, TGI, etc.).\n  Disables LS002 (prompt injection) since processing prompts is the engine's job.\n\n### Experimental rules\n\nBy default only [stable](#rule-stability) rules run. Add `--experimental` to also\nenable the heuristic rule LS002, or set `include_experimental: true` in\nyour config file:\n\n```bash\nllm-seclint scan . --experimental\n```\n\n### Inline Suppression\n\nSuppress specific findings with `# nosec` comments:\n\n```python\napi_key = \"sk-test-key-for-ci\"  # nosec LS001\n```\n\n## GitHub Code Scanning Integration\n\nllm-seclint supports [SARIF](https://sarifweb.azurewebsites.net/) output for direct integration with GitHub Code Scanning. Add this to your GitHub Actions workflow:\n\n```yaml\n- name: Run llm-seclint\n  run: llm-seclint scan . --format sarif -o results.sarif\n\n- name: Upload SARIF to GitHub\n  uses: github/codeql-action/upload-sarif@v3\n  with:\n    sarif_file: results.sarif\n```\n\nThe SARIF carries `properties.security-severity` on each rule (so Code Scanning\nrenders and sorts severity), and **taint-confirmed** findings include\n`properties.taint_source` + `confirmed_dataflow` on the result — so you can\nfilter the high-confidence \"this really is LLM/user input reaching a sink\"\nfindings in the GitHub Security tab.\n\n## Pre-commit Hook\n\nAdd llm-seclint to your `.pre-commit-config.yaml`:\n\n```yaml\nrepos:\n  - repo: https://github.com/xr843/llm-seclint\n    rev: v0.1.0\n    hooks:\n      - id: llm-seclint\n```\n\n## Configuration\n\nCreate a `.llm-seclint.yml` in your project root:\n\n```yaml\n# Patterns for files to include\ninclude_patterns:\n  - \"*.py\"\n\n# Patterns for files to exclude\nexclude_patterns:\n  - \"test_*.py\"\n  - \"*_test.py\"\n\n# Rules to ignore\nignore_rules:\n  - LS005\n\n# Minimum severity to report (CRITICAL, HIGH, MEDIUM, LOW, INFO)\nmin_severity: MEDIUM\n\n# Also run experimental (heuristic, higher false-positive) rules\ninclude_experimental: false\n```\n\n## Installation for Development\n\n```bash\ngit clone https://github.com/xr843/llm-seclint.git\ncd llm-seclint\npip install -e \".[dev]\"\npytest\n```\n\n## Contributing\n\nContributions are welcome! Here's how to add a new rule:\n\n1. Create a new file in `src/llm_seclint/rules/python/`\n2. Subclass `Rule` and implement the `check()` method\n3. Register the rule in `src/llm_seclint/rules/registry.py`\n4. Add tests in `tests/rules/`\n5. Update this README\n\nPlease open an issue first to discuss significant changes.\n\n## Roadmap\n\n- **v0.2** *(shipped)*: Intra-procedural taint tracking (real LLM/user → sink\n  data-flow) — every sink rule confirms dataflow and LS003 graduated to stable\n- **Next**: cross-function / control-flow-graph taint precision, so flows through\n  function parameters confirm (which would let LS002 graduate too)\n- **v0.3**: JavaScript/TypeScript analyzer (LangChain.js, Vercel AI SDK)\n- **v0.4**: Framework-specific rules (LangChain, LlamaIndex, Semantic Kernel)\n- **v0.5**: Auto-fix suggestions with `--fix` flag\n- **v1.0**: Stable API, VS Code extension\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxr843%2Fllm-seclint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxr843%2Fllm-seclint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxr843%2Fllm-seclint/lists"}