Observed since 20 Oct, 21:33 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Static analyzer for confused-deputy vulnerabilities in MCP, LangGraph, OpenAI Agents, and CrewAI systems
Static analysis that detects confused-deputy vulnerabilities in multi-agent and MCP-based AI systems — unvalidated paths where attacker-influenced data can reach a privileged action (shell exec, file write, credential access, network egress, agent delegation) with no trust boundary in between. It reads an agent/tool graph and reports where the unvalidated paths are, the way a SAST tool flags SQL injection before the query ever runs.
Supports LangGraph, the OpenAI Agents SDK, CrewAI, and MCP (both server manifests and
FastMCP @mcp.tool server source, the latter giving HIGH-confidence analysis of the real tool
bodies). Outputs a console table, JSON, or spec-compliant SARIF 2.1.0 for GitHub code scanning,
and returns a CI-friendly exit code.
Status: v1.1 (research/portfolio project). See DESIGN.md for the full rationale behind every decision below.
Single-agent LLM systems have one trust boundary: the line between the user's prompt and the model. Multi-agent systems (LangGraph, CrewAI, AutoGen, Google ADK) and MCP tool integrations multiply that boundary across a graph of agents and tools, and most frameworks wire that graph together without re-checking authorization at each hop. Concretely: data enters from a source that may be attacker-influenced — a scraped web page, a document pulled into RAG, a third-party API response, a poisoned tool result — and can flow, unvalidated, into a node with real-world write power.
This is a structural recurrence of the confused deputy problem (Norm Hardy, 1988), catalogued by MITRE as CWE-441 (Unintended Proxy or Intermediary). What's new is the medium: the "deputy" is now an LLM interpreting natural language, and the trust boundary is a graph edge instead of a function call — which makes the vulnerability harder to see by inspection and easier to construct by accident. The attack and runtime-defense research is mature; the static side (inspect the graph before deployment) is the gap this fills.
flowchart LR
subgraph Extraction
A1[LangGraph parser]
A2[OpenAI Agents / CrewAI parsers]
A3[FastMCP / MCP manifest parser]
end
B[Capability + source<br/>classification]
C[networkx graph]
subgraph Analysis
D[Trust-boundary<br/>detection]
E[Taint path<br/>enumeration]
F[Rule engine]
G[Severity + confidence<br/>scoring]
end
subgraph Reporting
H1[Console]
H2[JSON]
H3[SARIF]
end
A1 --> B
A2 --> B
A3 --> B
B --> C
C --> D
C --> E
D --> E
E --> F
F --> G
G --> H1 & H2 & H3
@mcp.tool), and MCP manifests into a normalized IR of nodes and edges. A .py file is parsed
against each framework (each ignores files that aren't its own). For LangGraph it AST-walks graph
construction (StateGraph, add_node, add_edge,
add_conditional_edges), the four tool-wiring idioms (ToolNode, create_react_agent,
create_agent, in-body llm.bind_tools), and modern multi-agent control flow
(Command(goto=...) and Send(...) handoffs, compile(interrupt_before/after=[...]) pause
points). Each node's calls — including those in local helper functions it calls
(interprocedural) — are recorded as evidence. Anything too dynamic to resolve is reported as
unresolved rather than silently dropped.CodeInterpreterTool/ShellTool (MEDIUM) > name/description keywords (LOW). Confidence is
reported alongside every finding.networkx digraph. A flat MCP manifest (no edges) gets a
synthesized client hub wired to every tool, modeling that one session can call them all.| Rule | What it flags | CWE (informal) |
|---|---|---|
| confused-deputy | An unmitigated path from an untrusted source to a privileged sink | CWE-441 |
| lethal-trifecta | One subgraph with untrusted input + private data + external egress (Willison) | CWE-668 |
| cyclic-delegation | An agent-to-agent delegation cycle that can bypass per-hop capability limits | CWE-441 |
| guardrail-only | A path whose only control is a prompt-based/sanitizer guard, not a structural boundary | CWE-863 |
| tool-injection | A tool whose own input flows unsanitized to a dangerous sink (intra-tool value taint) | CWE-78 / 918 / 22 |
Rules 1–4 are cross-agent reachability (can untrusted data reach a sink across the graph); rule 5 is intra-tool value taint (is a single tool's own input passed to a dangerous sink — a classic injection bug). See DESIGN.md §5a for why the inter-node analysis is reachability rather than value-tracking, grounded in the AgentFlow / VIPER-MCP literature.
Capabilities: CODE_EXEC, FILE_WRITE, FILE_READ, NETWORK_EGRESS, CREDENTIAL_ACCESS,
AGENT_DELEGATION, EXTERNAL_ACTION (financial/physical/publishing actions), READ_ONLY_QUERY.
Sources: UNTRUSTED_EXTERNAL, USER_INPUT, TRUSTED_INTERNAL.
What counts as a trust boundary (a path crossing one is mitigated): a human-in-the-loop
interrupt(); a deterministic validator with a rejection path (pydantic/jsonschema validation, or a
regex/allowlist check followed by raise); an OpenAI Agents SDK guardrail (tool_input_guardrails
/ output_guardrails, trusted as declared, not independently verified); or a node you explicitly
trust in the config. A bare sanitizer (re.sub, .strip) or an LLM "is this safe?" check is not
a boundary.
Requires Python 3.11+.
pip install agent-boundary-scan
Or from source:
git clone https://github.com/AureliusOctavion/agent-boundary-scan.git
cd agent-boundary-scan
pip install -e .
agent-boundary-scan scan <path> # a .py file, a .json manifest, or a directory
agent-boundary-scan scan src/ -f sarif -o results.sarif
agent-boundary-scan scan graph.py --fail-on CRITICAL
| Option | Meaning |
|---|---|
-f, --format | console (default), json, or sarif |
-o, --output | Write the report to a file instead of stdout |
--fail-on | Severity gate for the exit code: CRITICAL | HIGH | MEDIUM | LOW (default HIGH) |
--config | Path to an .agent-boundary-scan.yml (default: found next to the target) |
Exit codes: 0 clean, 1 a finding at or above the --fail-on severity was found, 2
usage/scan error. That makes scan usable directly as a CI gate.
1 finding(s) across 1 file(s): 1 critical
1. [CRITICAL] confused-deputy (confidence: high, CWE-441) confused_deputy_langgraph.py:31
Untrusted data (from: fetch_page) can reach privileged sink 'run_command' (CODE_EXEC) with no
trust boundary in between. Example path: fetch_page -> run_command.
path: fetch_page -> run_command
Severity maps to the SARIF level plus a numeric security-severity (so GitHub ranks findings);
confidence maps to properties.precision (the CodeQL convention).
{
"ruleId": "confused-deputy",
"level": "error",
"message": { "text": "Untrusted data (from: fetch_page) can reach privileged sink 'run_command' (CODE_EXEC) ..." },
"properties": { "security-severity": "9.0", "precision": "high", "cwe": "CWE-441" },
"locations": [ { "physicalLocation": {
"artifactLocation": { "uri": "confused_deputy_langgraph.py" },
"region": { "startLine": 31 } } } ]
}
.agent-boundary-scan.yml)# Nodes to trust as boundaries even if the static heuristics don't recognize them.
# Every path cleared this way is labeled "boundary trusted via user override" in output.
trusted_boundaries:
- my_module.custom_validator
- approval_node
# Severity at or above which `scan` exits non-zero (overridable with --fail-on).
fail_on_severity: HIGH
Keeping overrides in a checked-in file (rather than an in-code decorator) means they work for MCP-manifest-only scans too, and every trust decision is visible in one PR-reviewable place.
A reusable action ships in this repo. It installs the scanner, runs it, and gates the build; set
upload-sarif: true (with security-events: write) to feed findings into GitHub code scanning:
permissions:
security-events: write # only needed for upload-sarif
jobs:
agent-boundary-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AureliusOctavion/agent-boundary-scan@v1
with:
path: .
fail-on: HIGH
upload-sarif: true
Inputs: path (default .), fail-on (default HIGH), output (SARIF path), config,
upload-sarif (default false), python-version (default 3.11).
# .pre-commit-config.yaml
repos:
- repo: https://github.com/AureliusOctavion/agent-boundary-scan
rev: v1.1.1
hooks:
- id: agent-boundary-scan
Scans the whole tree on commit (the analysis is graph-based). Override the target or gate with
args: ["scan", "src/", "--fail-on", "CRITICAL"].
This is a triage/assist tool, not a proof of safety. A clean scan means "no obvious structural red flags, plus here is where I couldn't see" — never "safe". Key false-negative/false-positive risks (full discussion in DESIGN.md §7):
obj.foo())
is still under-reported.tool-injection rule.Fixtures under fixtures/ recreate the patterns (not verbatim code) of these
scenarios; the test suite asserts every vulnerable fixture is flagged and every safe fixture is not.
builder.compile() used as an add_node action
(Command/Send handoffs and interrupt_before/after pause points are already handled).@mcp.resource, @mcp.prompt) — resources as untrusted sources.pip install -e ".[dev]"
pytest -q # 166 tests
ruff check . # lint
CI (GitHub Actions) runs lint + tests on Python 3.11 and 3.12.
MIT
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 1.1.1 | 20 Sept 2026 | 6 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 21:33 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:bf7b1c286c7a81a5c3cd006bc8a81c679df8d31a7463292a27387171fab9a930