agentsec · Tools · mcpsink

Offline · stdlib-only · MIT

mcpsink: detect taint from MCP arguments into dangerous sinks in AI agents


Offline taint scanner for Python MCP server code. It finds where a tool's arguments end up.

What it detects

Offline taint scanner for Python MCP server code. It finds where a tool's arguments end up.

An MCP tool's arguments are chosen by the model, and the model can be steered by anything it has read: a web page, an issue, a poisoned document. So every @mcp.tool() parameter is attacker-controlled input. mcpsink finds the tool, resource and call_tool handlers in a FastMCP or official-SDK server, treats their parameters as tainted, follows them through assignments and same-file helper functions, and reports when they reach a dangerous sink: a shell, a file path, an outbound URL, SQL, eval/pickle, or os.environ. It also flags HTTP transports bound to 0.0.0.0 and tool annotations (readOnlyHint, destructiveHint) that contradict what the code does.

Other agentsec tools look at what the server *says* (toolpoison: descriptions; trifecta: capability mix; mcpposture: client configs). mcpsink reads the server's *implementation*.

Standard library only (ast). Nothing is imported or executed. No network.

Rules

RuleDefault severityFinding
MS01critical (shell) / high (program) / low (argv)Tool argument reaches os.system, os.popen, subprocess.*(shell=True), create_subprocess_shell (critical); chooses the program, or is passed as code to sh/python/node -c (high / critical); sits in an argv list without a preceding -- (low: option injection such as git --upload-pack)
MS02highTool argument used as a path for open, os.remove, shutil.*, os.listdir, Path(...).read_text/write_text/unlink/... without a containment check. With resolve()/realpath + is_relative_to/commonpath/startswith in the handler it drops to info
MS03highTool argument used as the URL/host for requests, httpx, aiohttp, urllib.request, http.client, socket or a client/session object (SSRF to 169.254.169.254/localhost, exfiltration via URL). A urlparse(...).hostname check drops it to info
MS04highTool argument in the SQL text of .execute()/executemany/executescript/text()/read_sql: f-string/%/+/.format built queries, or a tool that runs caller-supplied SQL
MS05criticalTool argument reaches eval, exec, compile, __import__/importlib.import_module, pickle/dill/marshal loads, yaml.load without SafeLoader, torch.load, jinja2.Template, runpy
MS06highos.getenv(arg) / os.environ[arg] with a tool-chosen name, or a tool that returns the whole os.environ
MS07mediumServer bound to all interfaces: host="0.0.0.0" / "::" in FastMCP(...), run(...), uvicorn.run(...), or *.host = "0.0.0.0"
MS08high / medium / lowreadOnlyHint=True on a tool that writes/deletes files (high) or runs processes (medium); destructiveHint=False on a tool that writes/deletes (medium); writes, deletes or runs processes with no annotation at all (low)
MS09infoInventory of handlers and the parameters treated as tainted

Usage

cd tools/mcpsink
python3 mcpsink.py examples/risky                 # 2 servers, 13 handlers, 21 findings, exit 1
python3 mcpsink.py examples/clean --fail-on any   # 9 handlers, 0 findings, exit 0
python3 mcpsink.py src/ --min-severity info       # include guarded sinks and the handler inventory
python3 mcpsink.py server.py --sarif > mcpsink.sarif
python3 mcpsink.py . --decorator 'register_tool' --json

After pip install of the kit, the same options work as agentsec mcpsink …. Every tool also accepts --help and almost every tool accepts --json.

Example output

See the examples under tools/mcpsink/examples/ in the repository.

SARIF and CI

agentsec mcpsink path/to/input --sarif > mcpsink.sarif

# GitHub Actions (composite action ships with the repo)
- uses: dunkleschitz/agentsec@v0
  with:
    tools: mcpsink
    paths: path/to/input
    fail-on: high

SARIF 2.1.0 can be uploaded to GitHub code scanning. The composite action writes per-tool reports under agentsec-sarif/ by default.

OWASP mapping

Heuristic mapping of what this tool helps a reviewer look for. Not an official OWASP endorsement, and not complete coverage of any item. Sources: OWASP Top 10 for LLM Applications 2025 and OWASP Top 10 for Agentic Applications 2026.

Known limits

comprehensions and calls to functions or methods defined in the same file (up to 3 levels). A helper's return value counts as tainted when any argument was. Taint is not followed across modules, through class attributes or globals, or through framework magic (a Pydantic model parameter is one tainted value).

per function by pattern, not proven to dominate the sink. An is_relative_to check anywhere in the handler downgrades every path finding in it. That is why guarded findings are info, not silent.

recognised; a custom sanitizer the tool doesn't know about produces a false positive (use the inline ignore).

Full source, tests, and fixtures: tools/mcpsink/ in the repository. Install the kit with pip install git+https://github.com/dunkleschitz/agentsec.git.