Palisade
A linter for LLM security. Palisade statically detects prompt-injection vulnerabilities in Python and JavaScript/TypeScript 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 .
Same output as text
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
| Rule | Path | Real-world precedent |
|---|---|---|
PI-EXEC |
input → LLM → exec / eval / new Function / vm.runIn* |
PandasAI, Langflow, LangChain PAL |
PI-SHELL |
input → LLM → os.system / subprocess(shell=True) / child_process.exec |
Open Interpreter (by design) |
PI-SQL |
input → LLM → raw non-parameterized SQL (cursor.execute, pool.query) |
Vanna.ai |
PI-FRAMEWORK-EXEC |
input → framework LLM wrapper (submit_prompt, generate_code, ...) → execution step |
Vanna.ai, PandasAI |
PI-HTTP |
input → LLM → model-chosen URL fetched (SSRF/exfil; advisory) | OWASP LLM Top-10 |
Sources cover Flask (request.*), FastAPI (@app.post route params and
pydantic bodies), Express (req.body/req.query), CLIs (input(),
sys.argv, process.argv) — and, in library mode, public function
parameters. Scanning the real vanna v0.5.5 with
--assume-params-untrusted flags exactly the CVE-2024-5565 sink
(base.py:1998) and nothing else.
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. - Several rules matching one
source → sinkpath? One finding — the most specific rule wins; no duplicate noise.
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
# with the JavaScript/TypeScript frontend (tree-sitter)
uvx --from "palisade-sec[js]" palisade-sec scan .
Python is scanned out of the box; .js/.ts/.tsx files are scanned when
the [js] extra is installed (otherwise they're skipped with a note).
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
palisade-sec fix . # remediation plan: guardrail + test per finding
palisade-sec fix
fix turns findings into a remediation plan (palisade-fixes.md): for each
finding, a rule-tailored guardrail (AST allowlist for exec, arg-list +
executable allowlist for shell, SELECT-only parser check for SQL, host
allowlist + private-IP block for SSRF) plus a pytest asserting the
guardrail blocks the canonical attack. Deterministic and offline — it
never modifies your code and never calls an LLM.
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 frontends ──────────────────▶ normalized taint IR
Python (stdlib ast) │
JS/TS (tree-sitter, optional extra) │
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 — proven, not promised: the
JS/TS frontend landed with zero engine changes, and the same YAML rules
match both languages (chat.completions.create, eval,
child_process.exec are just dotted paths). Go and more come the same way.
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.3.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.3.0.tar.gz | 102.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| palisade_sec-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 162.0 kB
Release files / palisade_sec-0.3.0.tar.gz
| Download URL | palisade_sec-0.3.0.tar.gz |
|---|---|
| Size | 102.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
44741fa31144bf3ea39dfb5fb9ab5ddb805b01a9c70ae71837d3e2db9eff20bf
|
|
BLAKE2b-256 checksum How to use checksums |
7c0239e9f49398f9321d7b1b26512bb4100f6f3f5131046ffe038387727cb794
|
| 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.3.0-py3-none-any.whl
| Download URL | palisade_sec-0.3.0-py3-none-any.whl |
|---|---|
| Size | 59.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f0b6f0ba535ddc5ac5829457360534bff0e788f4d78bb885fa783d4db512de32
|
|
BLAKE2b-256 checksum How to use checksums |
1a16aae2c19e1cb86c56141baad1d90e7f38f150d4c2c41738aa815ea2075f1d
|
| 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}
|