Palisade
A linter for LLM security. Palisade statically detects prompt-injection vulnerabilities in Python codebases — untrusted input flowing through an LLM into a dangerous sink — in CI, before they ship.
untrusted input → LLM → exec / shell / raw SQL (no sanitizer) ⇒ finding
No API key. No signup. No network calls. Pure static analysis.
uvx palisade-sec scan .
HIGH app.py:31 [PI-EXEC] Prompt injection reaching code execution
↳ source: question = request.json["question"] (app.py:31)
↳ llm: resp = client.chat.completions.create( (app.py:32)
↳ sink: exec(code) (app.py:40)
No sanitizer on path. Confidence: HIGH
Attack: crafted input makes the model emit Python that executes on your server.
Fix: never exec model output; sandbox + strict allowlist (denylists are bypassable).
Refs: CVE-2024-12366 (PandasAI); CVE-2025-3248 (Langflow, CISA KEV)
Why
This exact pattern is behind real, exploited CVEs: Langflow (CVE-2025-3248, on CISA KEV, exploited in the wild), PandasAI (CVE-2024-12366, CVSS 9.8), Vanna.ai (CVE-2024-5565), LangChain PAL/LLMMath chains (CVE-2023-36258, CVE-2023-29374). Almost nobody defends it at the code level: existing tools are runtime proxies (paid, in the traffic path) or guardrail libraries you have to know to wire in. Palisade is the missing piece — free, static, LLM-dataflow-aware, and CI-native, like ruff or semgrep but for the OWASP LLM Top-10 #1 risk.
What it detects (v1)
| Rule | Path | Real-world precedent |
|---|---|---|
PI-EXEC |
input → LLM → exec / eval / compile / PythonREPL |
PandasAI, Langflow, LangChain PAL |
PI-SHELL |
input → LLM → os.system / subprocess(shell=True) |
Open Interpreter (by design) |
PI-SQL |
input → LLM → raw non-parameterized SQL | Vanna.ai |
Palisade runs taint analysis, not grep: it only reports a complete
source → LLM → sink data-flow path with no sanitizer in between.
- Constant developer prompt → LLM →
exec? Silent — no untrusted source. subprocess.run([...])with an arg list? Silent — safe sink shape.- Parameterized
cursor.execute(q, params)? Silent. - Allowlist / pydantic validation on the path? Silent — sanitized.
- Denylist or human-confirmation gate? Flagged MED "risky" — real CVEs were exploited despite exactly those defenses. That is deliberate.
- A "sanitizer" in name only — a project function matching
sanitize/validatewhose body never actually validates? Flagged MED "unverified sanitizer" — Vanna's cosmetic_sanitize_plotly_codeshipped CVE-2024-5565 straight through such a function.
Install & run
# one-shot, no install
uvx palisade-sec scan path/to/project
# or
pipx run palisade-sec scan .
# or as a dev dependency
uv add --dev palisade-sec
Useful flags:
palisade-sec scan . --all # also show MED/LOW findings
palisade-sec scan . --json # stable machine-readable output
palisade-sec scan . --report # write palisade-report.md
palisade-sec scan . --rules ./my-rules # add your own YAML rules
palisade-sec scan . --assume-params-untrusted # library mode, see below
Scanning libraries
Apps read untrusted input from request.* / input() / sys.argv. A
library has no visible caller — its public parameters ARE the untrusted
world (Vanna's ask(question), CVE-2024-5565). Library mode treats the
parameters of public (non-underscore) functions as untrusted sources:
palisade-sec scan path/to/library --assume-params-untrusted
If the library routes LLM calls through its own wrapper method, add the
wrapper to a custom rule's llm_signatures (e.g. "*.submit_prompt") — see
the rules guide.
CI
Gate pull requests on new findings only — adopt Palisade on an imperfect codebase without a wall of pre-existing failures:
palisade-sec baseline . # once; commit .palisade/baseline.json
palisade-sec scan . --ci --baseline .palisade/baseline.json
--ci exits non-zero only if a new HIGH finding appears. Fingerprints are
line-number independent, so refactors don't churn the baseline.
GitHub Actions:
- uses: astral-sh/setup-uv@v5
- run: uvx palisade-sec scan . --ci --baseline .palisade/baseline.json
Configuration
pyproject.toml:
[tool.palisade]
paths_ignore = ["migrations/*", "sandbox/*"]
include_tests = false # tests/** and conftest.py are skipped by default
max_hops = 3 # inter-procedural depth bound
assume_params_untrusted = false # library mode (see "Scanning libraries")
Or the same keys in .palisade.toml.
Custom rules
Rules are plain YAML validated by a pydantic schema — sources, LLM call
signatures, sinks, sanitizers, partial defenses. Adding coverage for a new
framework is a small PR with no engine changes. See
src/palisade_sec/rules/README.md for
the 5-minute guide.
Architecture
source ──▶ language frontend (Python: stdlib ast) ──▶ normalized taint IR
│
language-agnostic engine ─┤ taint propagation,
sanitizer resolution, confidence scoring
│
YAML rules ──▶ findings ──▶ baseline diff ──▶ terminal / json / md
The frontend/IR split is the scalability story: JS/TS/Go land later as new frontends (tree-sitter) with zero engine changes.
Safety of the tool itself
- Palisade never executes, imports, or evaluates scanned code — it only
parses source text with
ast.parse. scanmakes no network calls and needs no API key or account.- No telemetry. Nothing leaves your machine.
An honest note on scope
Palisade is one layer of defense against one class of vulnerability. A clean scan means no detected injection-to-sink path — it does not mean your application is secure. Keep your runtime guardrails, permissions boundaries, and sandboxes; Palisade complements them, before merge.
License
MIT
Release files for palisade-sec 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| palisade_sec-0.2.0.tar.gz | 71.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| palisade_sec-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 114.2 kB
Release files / palisade_sec-0.2.0.tar.gz
| Download URL | palisade_sec-0.2.0.tar.gz |
|---|---|
| Size | 71.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4d0fa5d5b114d3c393997f06e7a612af0e1c4d24e0b7126fa76b606289bd5bc1
|
|
BLAKE2b-256 checksum How to use checksums |
451a5c1405f7618063b3304090d318af60aea05a65e10914e84c620dba4c5e32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / palisade_sec-0.2.0-py3-none-any.whl
| Download URL | palisade_sec-0.2.0-py3-none-any.whl |
|---|---|
| Size | 43.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d2a3dc9b8c6e4e95c1080069801dd9623f5c3b285b3325618d8e63485cc9214
|
|
BLAKE2b-256 checksum How to use checksums |
bdc065d90236042e258bc0e9ff7046c92ee495414d374fb3588b07d224f23f36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|