Skip to content

gateway: withhold poisoned tool results by default (was inert) + stop echoing the payload - #318

Open
Ar9av wants to merge 1 commit into
mainfrom
fix/gateway-withhold-untrusted-output
Open

gateway: withhold poisoned tool results by default (was inert) + stop echoing the payload#318
Ar9av wants to merge 1 commit into
mainfrom
fix/gateway-withhold-untrusted-output

Conversation

@Ar9av

@Ar9av Ar9av commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The MCP gateway's headline feature — "every tool result is injection-scanned before the model sees it" — was inert by default, and when it did fire it echoed the payload back. Two fixes.

1. Result-withholding was gated on the wrong thing

_result_withhold_finding only fired on findings whose global rule mode was enforce. But prompt-injection / prompt-injection-hidden ship at their observe default (they're not in the recommended enforce floor). So even with the gateway in --mode enforce, a poisoned tool result sailed through.

Reproduced with a fake upstream MCP server that returns an IGNORE ALL PREVIOUS INSTRUCTIONS … exfiltrate the ssh key payload:

--- CALL fetch_notes (poisoned result), prompt-injection at observe default ---
BEFORE:  isError: false   ← full injection forwarded to the model, only logged
AFTER:   isError: true    "[Prismor] response withheld (rule: prompt-injection)"
benign echo: passes through unchanged in both

Fix: a tool RESULT is untrusted content from an external server — a distinct trust boundary from the user's own prompts. Withholding a poisoned/secret-leaking result is the exact mitigation and, unlike blocking a call, cannot false-positive on anything the user wrote. So the gateway now withholds any non-inert finding in the curated categories (injection, secret access/exfil, data-boundary, pii) on a post-action result under gateway enforce mode, independent of the rule's global mode. contextInert matches and non-withhold categories are still skipped.

2. The withhold message echoed the payload

_blocked_result appended evidence. For a result the evidence is the poisoned output, so the "response withheld" error handed the model the very injection it removed. Added include_evidence=False on the withhold path; pre-call denials keep evidence (the agent's own command, safe and useful to show).

Testing

  • End to end against a fake malicious MCP upstream (details above): poisoned result withheld with the injection text gone; benign call unaffected.
  • tests/test_mcp_mirror.py: 34 passed — updated so observe-mode result injection now withholds, added evidence-suppression coverage (and that pre-call denials still show evidence).

This was surfaced by direct testing; it does not change any pre-call behavior or the false-positive surface on the user's own prompts/commands.

…al mode

The MCP gateway's headline feature — scan every tool result and withhold
injection / leaked-secret output before the model sees it — was inert by
default. `_result_withhold_finding` only fired on findings whose global rule
mode was `enforce`, but `prompt-injection`/`prompt-injection-hidden` ship at
their observe default (not in the recommended floor). Result: a fake upstream
returning an "IGNORE ALL PREVIOUS INSTRUCTIONS ... exfiltrate the ssh key"
fetch-and-execute payload was forwarded to the model verbatim (isError:false),
only logged.

A tool RESULT is untrusted content from an external server — a distinct trust
boundary from the user's own prompts/commands. Withholding a poisoned or
secret-leaking result is the exact mitigation and, unlike blocking a call,
cannot false-positive on anything the user wrote. So the gateway now withholds
any non-inert finding in the curated categories (injection, secret access/exfil,
data-boundary, pii) on a post-action result under gateway enforce mode,
independent of the rule's global observe/enforce setting. contextInert matches
and non-withhold categories are still skipped.

Also: the withhold message no longer echoes `evidence`. For a result the
evidence IS the poisoned output, so echoing it back re-introduced the very
injection/secret the withhold removed. Added include_evidence=False on that
path; pre-call denials keep evidence (the agent's own command, safe to show).

Verified end to end against a fake malicious MCP upstream with prompt-injection
at its observe default: poisoned fetch_notes -> isError:true "[Prismor] response
withheld (rule: prompt-injection)" with the injection text gone; benign echo
passes. Tests updated in test_mcp_mirror.py.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant