Catalog PyPI
PACKAGE EXPLORERView in registry

agent-boundary-scan

PyPI

Static analyzer for confused-deputy vulnerabilities in MCP, LangGraph, OpenAI Agents, and CrewAI systems

confused-deputylanggraphllm-agentsmcpsastsecuritystatic-analysis
Versions collected1
Direct dependencies6From release 1.1.1
Last observed20 Oct, 21:33 UTC

README

Version 1.1.1 documentation

agent-boundary-scan

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.

The problem

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.

How it works

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
  1. Extraction parses LangGraph source, OpenAI Agents SDK source, FastMCP server source (@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.
  2. Classification assigns each node capabilities and a source category using a signal hierarchy: AST-detected calls (HIGH) > MCP annotation hints / known dangerous tool classes like CodeInterpreterTool/ShellTool (MEDIUM) > name/description keywords (LOW). Confidence is reported alongside every finding.
  3. Graph construction assembles a 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.
  4. Analysis detects trust boundaries, enumerates source→sink paths that don't cross one, and applies five rules (four graph-reachability + intra-tool value taint).
  5. Reporting renders findings as console / JSON / SARIF and sets the exit code.

What it detects

RuleWhat it flagsCWE (informal)
confused-deputyAn unmitigated path from an untrusted source to a privileged sinkCWE-441
lethal-trifectaOne subgraph with untrusted input + private data + external egress (Willison)CWE-668
cyclic-delegationAn agent-to-agent delegation cycle that can bypass per-hop capability limitsCWE-441
guardrail-onlyA path whose only control is a prompt-based/sanitizer guard, not a structural boundaryCWE-863
tool-injectionA 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.

Installation

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 .

Usage

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
OptionMeaning
-f, --formatconsole (default), json, or sarif
-o, --outputWrite the report to a file instead of stdout
--fail-onSeverity gate for the exit code: CRITICAL | HIGH | MEDIUM | LOW (default HIGH)
--configPath 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.

Console output

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

SARIF (for GitHub code scanning)

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 } } } ]
}

Configuration (.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.

GitHub Action

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 hook

# .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"].

Limitations (read these)

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):

  • Heuristic classification (LOW-confidence, keyword-based) both under- and over-classifies; confidence is always reported so you can weigh it.
  • Interprocedural analysis is module-local and bare-name only — a node is scanned together with local helpers it calls, but danger reached only through imported code or method calls (obj.foo()) is still under-reported.
  • MCP-only scans have no function bodies to inspect, so confidence is capped at MEDIUM/LOW, and the co-reachability model assumes all of a manifest's tools are exposed to one agent (can over-flag).
  • MCP annotation hints are server-supplied claims, treated as untrusted per the MCP spec.
  • Cyclic-delegation deliberately excludes agent↔tool ReAct loops (can miss a cycle through an agent that also wraps tools).

Prior art

  • pajaMAS — Trail of Bits (Triedman et al., 2025): MAS-hijacking control-flow attacks.
  • InjecAgent (ACL Findings 2024) and AgentDojo (NeurIPS 2024): indirect-prompt-injection benchmarks for tool-using agents.
  • Prompt Flow Integrity (Kim, Choi, Lee, 2025) and MiniScope (2025): runtime enforcement / least-privilege reconstruction — complementary to this tool's static, pre-deploy analysis.
  • OWASP Top 10 for Agentic Applications (2026) and Simon Willison's lethal trifecta (2025).
  • "Capability Gates Are Not Authorization" (arXiv 2606.28679, 2026): frameworks gate which tools a model sees but don't re-authorize the specific arguments of a specific call — the gap confused-deputy paths exploit.
  • AgentFlow (arXiv 2607.01640, 2026): the first static analysis framework for agent programs — independently validates this tool's approach (LLM as a taint conduit; reachability over an over-approximating dependency graph).
  • VIPER-MCP (arXiv 2605.21392, 2026): intra-tool taint of MCP servers (106 zero-day CVEs) — the basis for the 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.

Future work

  • Deeper interprocedural resolution — follow method calls and imported helpers (needs light type/import resolution), beyond today's module-local bare-name expansion.
  • Compiled subgraphs as nodes — inline a builder.compile() used as an add_node action (Command/Send handoffs and interrupt_before/after pause points are already handled).
  • MCP resources & prompts (@mcp.resource, @mcp.prompt) — resources as untrusted sources.
  • Full argument-level dataflow — track the untrusted value into the sink's dangerous argument (today's taint is graph-reachability with self-node and per-sink precision refinements).
  • AutoGen / ag2, Google ADK, Pydantic AI extraction, and cross-framework tracing.
  • CrewAI Task/context chaining (v1 models agents, tools, and delegation, not task data-flow).
  • SARIF codeFlows — render the full source→sink chain as thread-flow locations.
  • Correlating an MCP manifest with the server implementation source when available (upgrades MCP-only confidence from MEDIUM to HIGH).

Development

pip install -e ".[dev]"
pytest -q          # 166 tests
ruff check .       # lint

CI (GitHub Actions) runs lint + tests on Python 3.11 and 3.12.

License

MIT

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
1.1.120 Sept 20266

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
aureliusoctavion/agent-boundary-scan

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 21:33 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:bf7b1c286c7a81a5c3cd006bc8a81c679df8d31a7463292a27387171fab9a930