Skip to content

AI/agent local-surface week: MCP DNS-rebinding, OAuth-subject wildcards, expression-sandbox escapes, and tenant-binding drift (44 GHSAs)

Source: hourly offensive-security scan of GitHub Security Advisories on 2026-09-15 (waves published 2026-09-09T23:43Z–2026-09-15T21:33Z).

A dense week across the agent/MCP/LLM-tooling surface, with three reusable axes:

  1. Local MCP/dashboard listeners are browser-reachable. Three advisories show the same shape — a tool server bound locally or on 0.0.0.0 with no Host/Origin validation, so DNS rebinding preserves an attacker-controlled Host/Origin while routing browser requests to the victim's localhost listener (@zereight/mcp-gitlab, mysql-mcp-server SSE, Mockoon admin API with wildcard CORS).
  2. An alternate authentication path skips the checks the primary path enforces. Open WebUI's OAuth token-exchange endpoint skipped the domain allowlist and role management the normal callback runs; n8n's disabled-OIDC endpoints kept issuing sessions; n8n's model-search route skipped the credential's allowed-domains check the main request path applies; Open WebUI's SQLite subject lookup turned provider subjects into wildcard matches.
  3. Code-generation and template stages that resolve globals at generation time can be poisoned. n8n's expression sandbox fell twice to same-theme escapes: a dynamically-scoped sanitizer resolvable through a class field (__sanitize rebinding), and code printers calling global JSON.stringify at generation time (replace the global → alter the executed source).

Cluster A — MCP and local tool servers (fixes available)

GHSA CVE Sev. Boundary
GHSA-vmp7-252j-cwp7 CVE-2026-61568 critical @zereight/mcp-gitlab < 2.1.30: Streamable HTTP MCP endpoint with no effective Host/Origin allowlist → DNS rebinding reaches the local MCP transport (CWE-350)
GHSA-2h44-8472-frjj CVE-2026-61559 critical @zereight/mcp-gitlab < 2.1.27: with ENABLE_DYNAMIC_API_URL=true, the X-GitLab-API-URL request header selects the outbound API base URL (well-formedness check only) and the operator's Private-Token is attached to the redirected fetch — header-steered credential-bearing SSRF
GHSA-5648-rgj9-v224 — high @zereight/mcp-gitlab < 2.1.30: execute_graphql defeats BOTH read-only mode and GITLAB_ALLOWED_PROJECT_IDS; unauthenticated /mcp under cookie-jar/device-flow credentials; unauthenticated SSE; unauthenticated session/transport enumeration
GHSA-cv3r-c5h8-f4g5 CVE-2026-61560 critical Sept 16 follow-up: @zereight/mcp-gitlab < 2.1.27 SSE-mode full chain — SSE=true (the default Docker deployment per docker-compose.yaml) exposes all ~100+ tools with zero auth (and REMOTE_AUTHORIZATION is explicitly incompatible with SSE), while upload_markdown reads arbitrary files via unsanitized file_path and uploads them to a GitLab project: unauthenticated attacker grabs /proc/self/environ (plaintext GITLAB_PERSONAL_ACCESS_TOKEN), retrieves it from the project's /uploads/ URL, and achieves full account takeover. Dockerfile runs as root, compose maps 3002:3002 on 0.0.0.0. Validation stays lab-bounded: disposable instance + fake PAT, read a synthetic canary file — never /proc/self/environ on a live deployment
GHSA-rqfv-2mw9-78g2 CVE-2026-59971 critical mysql-mcp-server < 0.4.2 in MCP_TRANSPORT=sse: MCP Python SDK DNS-rebinding protection not enabled, no TrustedHost/CORS middleware, binds 0.0.0.0, no auth → unauthenticated execute_sql (full dump; FILE privileges → file read/write)
GHSA-65h7-9wrw-629c CVE-2026-59973 high FrontMCP / mcp-from-openapi 2.3.0: latest-version bypass of the prior external-$ref SSRF fix — hostname denylist misses loopback reached via name resolution, redirects, and IPv4-mapped IPv6
GHSA-rqx4-3f6q-3x2v CVE-2026-59148 high @mockoon/commons-server < 9.7.0: unauthenticated admin API on the same listener as mock routes, Access-Control-Allow-Origin: * with all methods, reads every MOCKOON_* env var and hijacks mock state
GHSA-8wqc-v2q8-vff2 CVE-2026-59149 medium Mockoon FILE response with templated filePath confined by startsWith(staticBaseDir) with no separator boundary — sibling-prefix escape reads outside the served directory
GHSA-hf57-cqmx-p4gr CVE-2026-88062 critical OmniRoute ≤ 3.8.50: POST /api/acp/agents accepts attacker-controlled binary and versionCommand; the only validation checks the first token of versionCommand equals binary — also attacker-controlled → execFileSync of arbitrary Node.js
GHSA-wfgq-w7cq-qj7j — high mistral.rs ≤ 0.8.17 media loader: unauthenticated image_url/audio_url fetched with no host/IP validation, and bare existing paths opened as local files → SSRF + arbitrary local file read
GHSA-rrxg-g2pf-6hh4 CVE-2026-59178 critical ESPHome Device Builder < 1.0.12: auth env vars renamed (USERNAME/PASSWORD → ESPHOME_*) with no fallback → dashboards protected under the old guide silently start unauthenticated on upgrade
GHSA-vv4j-m4vr-f3g6 CVE-2026-59177 high ESPHome HA add-on ingress dashboard bound to all interfaces without auth

Cluster B — Open WebUI session/fetch/authorization boundaries (fixed 0.11.1)

GHSA CVE Sev. Boundary
GHSA-wpmr-8h3q-fwj7 CVE-2026-87016 high SQLite subject lookup does a substring match → a subject containing SQL wildcards (%) matches other accounts (admin first) — OAuth/SCIM identity binding via wildcard subjects
GHSA-4qg5-cxx4-g927 / GHSA-wvm9-9g5j-623f CVE-2026-88005 / 88006 medium OAuth token-exchange endpoint skips the email-domain allowlist and role management the normal login callback enforces → denied/demoted users retain sessions
GHSA-jmc6-2wr8-h3wj CVE-2026-87995 high Port-preview iframe hardcodes allow-same-origin + allow-scripts on an app-origin path → same-origin XSS → session-token theft/account takeover
GHSA-34r3-9m95-vq73 CVE-2026-87999 high SSRF external-destination screen used Python "globally routable" → reserved internal classes answer yes, including 168.63.129.16 (Azure platform channel reachable from every Azure VM); any authenticated user reads responses back
GHSA-2724-6cpj-gf3v CVE-2026-87998 high Non-admin knowledge-base deletion destroys admin-owned external knowledge connections
GHSA-pcvc-8vrv-8q6w CVE-2026-87017 medium Built-in knowledge tool exposes inaccessible knowledge bases on most vector backends
GHSA-wjwr-xfp9-r66p CVE-2026-87014 medium SSO-demoted admin keeps cached socket role → continued all-users notes read/write over already-open Socket.IO connections
GHSA-p78m-89r6-pgf7 CVE-2026-87015 medium User session cookies leak to external tool servers configured for bearer auth (cookie jar read late, last-connection wins)
GHSA-3g9q-v48f-hh9w CVE-2026-87011 high Unauthenticated OIDC back-channel logout endpoint fetches provider discovery/JWKS uncached per request, blocking call in the event loop → trivial unauth stall (availability; included for the fetch-on-unauth-token pattern)
GHSA-fmqh-xp37-5hr8 / GHSA-3pf7-q2g3-wj28 CVE-2026-87994 / 87997 medium Chat-completions endpoint lets channel members overwrite others' messages / inject chats into other users' folders — endpoint-level object-binding drift

Cluster C — n8n workflow-platform boundaries (fixed 1.123.76 / 2.37.7 / 2.38.2)

GHSA CVE Sev. Boundary
GHSA-hw8v-xxg5-vvvx CVE-2026-86076 high Expression sanitizer resolved through dynamically-scoped this; a class field named __sanitize rebinds it → Function constructor, code execution in the n8n process and editor-preview XSS
GHSA-6xcw-7xm6-48c6 CVE-2026-86083 high Code-generation stages call global JSON.stringify at generation time; expression that replaces the global alters subsequently generated executed source (legacy engine)
GHSA-35jj-42hp-8gmq CVE-2026-86077 medium /chat WebSocket resumed any paused execution from an anonymous form resumeToken without checking node type → anonymous release of approval-gated actions
GHSA-cw9w-vv67-hf73 CVE-2026-86073 medium OAuth refresh tokens not bound to the consented resource → refresh into a token for a different, unapproved workflow
GHSA-qgpw-8g46-w95v CVE-2026-86995 medium Git node setUpstream wrote unvalidated branch.<name>.remote into repo config; later fetch resolved the remote from config → read any local repository the process can access
GHSA-34ff-336r-5q23 CVE-2026-86082 high OpenAI Chat Model node checked custom base URL against allowed-domains only on the main path; the model-search endpoint sent options.baseURL to arbitrary hosts with the credential attached
GHSA-pf83-w3f9-8m37 CVE-2026-86084 medium Disabled OIDC endpoints still ran the full flow and issued sessions
GHSA-5m98-cgcr-xx3q CVE-2026-86080 medium GitHub Trigger 422 reuse path adopted an existing webhook but dropped the signing secret → signature verification fail-open, deliveries accepted from anyone
GHSA-pq6c-vh67-xpm3 CVE-2026-86993 medium Log-streaming destinations decrypted any named generic-auth credential without ownership check → cross-project secret to an attacker endpoint
GHSA-cqr2-h44g-v75v CVE-2026-86085 medium Role-assignment endpoints checked role-type permission but not project scope → cross-tenant member PII
GHSA-hh89-3r9w-qj3j / GHSA-j535-v25q-vx3q CVE-2026-86075 / 86081 high Unauth unbounded persistent writes via OAuth DCR fields; ReDoS in default blocked-file pattern (availability siblings — tracked)

Cluster D — identity/tenant and misc boundaries

GHSA CVE Sev. Boundary
GHSA-h8m9-jgf8-vwvp CVE-2026-59151 critical Prowler < 5.30.3: SAML tenant binding derived from the asserted email domain instead of the validated SAML configuration → a malicious tenant with its own IdP mints tokens for another tenant (cross-tenant ATO)
GHSA-vrh8-c9cm-wh8v CVE-2026-56668 high ZITADEL < 4.15.3: OAuth2 token-exchange never validated that the submitted token belongs to the exchanging client, and no scope-subset enforcement → low-privilege token exchanged into another application's elevated token
GHSA-v77h-2w3m-94hx — medium ZITADEL JWT IdP provider: exp not validated (replayable IdP assertion) — adjacent tracked
GHSA-243p-f3cv-c5wh + GHSA-j328-xmgp-j4q3, GHSA-g3f9-g5vj-p62f, GHSA-2cg9-97gq-9mqp CVE-2026-56827/56828/56829/56825 high Shopper < 2.9.2: Filament/Livewire admin components expose bulk actions, stock mutation, and privilege paths missing per-role authorization — the Livewire component-level authz-audit pattern
GHSA-4x45-gxvp-6283 CVE-2026-59960 high @argos-ci/core: CI branch name interpolated into shell command → OS command injection from repository-controlled branch names (repo-text-to-shell axis)
GHSA-x8vr-98rv-c92v CVE-2026-10144 high Rsbuild < 2.0.9 macOS server.open: encodeURI() does not encode $, (, ), ; → shell metacharacters in a crafted URL reach child_process.exec

Processed without publication this run (sparse/product-specific or availability-only): the Sept 15 Chrome 153 GHSA mirror wave (memory-safety, no public exploit path), Microsoft .NET/VS wave, Arista EOS IS-IS/VRRP/DHCP protocol DoS set, Netcore NR255/NR268 firmware bundle set (single-product CGI bugs; command-construction axis already covered by existing appliance pages), mySCADA/myPRO Manager unauth command API and hardcoded-key records (OT appliance, tracked in KEV-style notes only), Velocloud Edge input-validation/update-signature records (vendor embargo), October CMS safe-mode/session-store trio, Cotonti unserialize(), yayson prototype pollution, EspoCRM weak rand() tokens, Excelize allocation, GIMP file-psd, Podman podman load tar, adm-zip decompression limits, libp2p-quic panic, emp3r0r polling DoS, GeoNetwork SLD SSRF, Nuxt Ollama key exposure, turbo-graph /api/run, Joker project-local config exec, functype-mcp pnpm alias RCE, openhop flow-ID traversal, Identrail installation_id IDOR, docx-editor-react font-family CSS injection, QloApps back-office XSS, Devolutions log disclosure, ZITADEL role-revocation/email-verification items, Shopper minor siblings.

Why this is worth an operator page

  • DNS rebinding vs local MCP/dashboard listeners is now the default attack path for developer workstations. Three separate ecosystems (GitLab MCP, MySQL MCP, Mockoon) proved the same missing control — Host/Origin validation at the HTTP boundary — in one week. If a local tool speaks HTTP, treat the browser as a remote attacker.
  • Header-steered, token-attaching fetches (X-GitLab-API-URL + Private-Token) are the cleanest recent example of a "convenience" env flag turning a credential store into an SSRF payload delivery mechanism.
  • The alternate-path bypass axis generalizes: token-exchange vs login callback, model-search vs main request, disabled-but-live OIDC, 422-reuse vs fresh-registration webhook paths. Enumerate every route family that reaches the same authority decision.
  • SAML-derived-tenant from asserted email domain and token-exchange missing token-ownership check are both immediately testable in any multi-tenant IdP-backed app.

Validation workflow (authorized scope only)

Disposable servers and fake credentials only

Run all proofs against disposable MCP/dashboard/mock instances you own with fake database credentials, synthetic workflows/projects, and throwaway tenants. Use rebinding only against your own lab hosts. Never invoke execute_sql-class tools against real databases, never attach real GitLab/cloud tokens to redirect proofs, never touch other tenants' data.

  1. Local-listener rebinding matrix. For each locally-bound tool server: enumerate binds (127.0.0.1 vs 0.0.0.0), Host/Origin validation, auth requirements per transport (stdio vs SSE vs Streamable HTTP). Rebind an owned domain at 301s TTL to the lab listener; record whether init/tool-list succeeds from the browser context.
  2. Header/url-selector grep. In any agent server that lets config enable dynamic endpoints, grep for request headers or tool arguments reaching fetch/axios base-URL and check whether a stored credential is attached to that final URL. Prove with an owned no-content listener and a fake token; expect the fake token in the request log.
  3. Alternate-path parity test. For any auth/authz control, script a positive/negative decision table across every entry route (primary callback, token exchange, disabled provider, admin alternate routes). Divergence is the finding.
  4. Wildcard-subject check (owned IdP + owned accounts only): assert subjects containing %/_ and confirm lookup semantics; SQLite vs Postgres path differences matter.
  5. Reserved-IP SSRF class list. When testing URL screens, include the reserved-but-not-private classes (e.g. 168.63.129.16 on Azure VMs, cloud metadata equivalents you are explicitly authorized to probe); stop at response-disclosure evidence.
  6. Sandbox-escape grammar inventory (n8n-class platforms): document which globals the codegen path resolves at runtime (JSON.stringify, sanitizer names, this-scoped lookups) in the deployed version before theorizing escapes; treat the two published themes as grammar seeds, not payloads.

September 18 follow-up: process-compose MCP SSE rebinding + ToolHive host.docker.internal lateral movement

  • process-compose MCP SSE DNS rebinding (CVE-2026-77339, GHSA-5gm3-9crp-6g3v): when mcp_server.transport is omitted, SSE is the default transport, configured with only host+port — no auth, Host, Origin, or caller-secret field exists. The X-PC-Token-Key middleware is installed on the Gin REST router only; the MCP listener is started separately and does not inherit it. A rebinding page (Host/Origin = attacker domain, resolves to 127.0.0.1) reaches the SSE transport before any validation and dispatches into the registered control tools: start/stop/restart/scale processes, read/search/truncate logs, and project/process state disclosure. New axis for the rebinding matrix: when a tool exposes two listeners (REST API + MCP), test auth per-listener — the product's token middleware covering the API proves nothing about the MCP port. Prove with your own lab process-compose + owned rebinding domain; exercise the state-disclosure tools only.
  • ToolHive host.docker.internal lateral movement (CVE-2026-58197, GHSA-qg2g-g9w3-m5h8, CVSS 8.8): the default network permission profile is insecure_allow_all: true, so any containerized MCP server thv run under defaults can POST to http://host.docker.internal:50444/mcp — the ToolHive control plane itself (full MCP handshake succeeds) — plus every other managed MCP proxy and any host-localhost service, with no container escape required. Audit rule for MCP runners/supervisors: after landing inside one managed server container (or via its tools), probe the host-local supervisor control plane and sibling proxies before theorizing an escape; the supervisor's own API may be unauthenticated by design. Lab-bounded: disposable mcp/filesystem container + synthetic canary files.

September 20 follow-up: nexus-mcp reauth tool builds a shell string from the url argument

  • nexus-mcp nexus_reauth MCP tool command injection (CVE-2026-94031, GHSA-c2f2-x95m-p9vw, exploit public): the auth helper (src/auth/browser.ts) passes the caller-supplied url argument straight into a child_process.exec string — the MCP-tool twin of the CLI-front-end argv rule. Operator audit for any MCP server with a browser/open/auth tool: grep the transport handlers for exec/execSync/os.system and check whether the tool argument lands in a shell string versus an argv array with a fixed executable. A tool argument is shell input until an argv-array (or a URL-parsed-then-quoted) implementation is proven; capture the constructed command string with an inert option-shaped canary, never execute it. Lab-bounded: disposable MCP server + fake credentials only.

September 20 follow-up: BrowserSkill local-daemon origin allowlist forgeable by extension registration

  • Tencent BrowserSkill ≤ 0.3.0 local daemon WebSocket origin check (CVE-2026-94111, GHSA-x6f2-qp26-2xrh): the daemon accepts any chrome-extension:// origin whose ID is 32 characters drawn from [a-p] — a character-set + length predicate, not an exact allowlist. Because Chrome extension IDs are derivable at key-generation time, an attacker mints an in-range key offline, publishes (or sideloads) a malicious extension, and registers as a browser client on the local daemon: it then intercepts and manipulates page content, DOM, and screenshots returned to the AI agent — a man-in-the-browser on the agent's entire perception channel. Operator rule for every browser-bridge / local-agent daemon origin gate: test whether the check is an exact ID allowlist or a shape predicate (prefix, charset, length); if shape-based, generate a passing ID before concluding the gate works. Lab-bounded: your own disposable extension with a brute-forced in-range key against a local daemon you own; prove registration + one benign DOM read, never victim sessions.

September 21 follow-up: containment guard anchored to a caller-supplied root enforces nothing

  • ACE-MCP ≤ 4.10.8 get_file_snippet path traversal (CVE-2026-94046, GHSA-8wj2-8g8v-vcxr, exploit public): isPathInsideProjectRoot only blocks filePath from escaping the caller-supplied projectRootPath; the root itself is untrusted client input — set projectRootPath=/etc, filePath=hosts and the guard passes. Operator rule for every MCP/file tool that takes a root/base/dir argument paired with a path argument: containment checked across a pair where both sides are attacker-chosen proves nothing. Re-anchor mentally: is any leg of the containment comparison sourced from server-fixed config? If not, sweep sibling roots (/etc, ~/.ssh, app config dirs) by simply choosing the root, no traversal characters needed. Lab-bounded: disposable MCP server + synthetic canary files outside the intended project root.
  • Same-wave MCP-surface singles worth probing during any MCP engagement: 03-lovepreetSingh MCP create_file writes content to an unsandboxed filePath (CVE-2026-94044, 7.3, GHSA-wvg2-xpp5-j725); MCPHub ≤ 1.0.32 importTemplate improper-privilege drift on the template-import endpoint (CVE-2026-94047, 6.3, GHSA-2xcr-p42x-hfx6); cowork_bench pdf-tools-mcp pdf_file_path SSRF (CVE-2026-94051, GHSA-6j48-ggfg-8xw2) — a PDF-tool path/file argument is a fetch/file sink: test it against an owned callback and file://-shaped local paths, not just real PDFs.

September 22 follow-up: MCP tool writes with caller-supplied directory AND caller-supplied filename components

  • @roomi-fields/notebooklm-mcp vault_batch / POST /batch-to-vault arbitrary file write (CVE-2026-61647, GHSA-jjhp-8crj-mppq, high): the vault_dir tool argument went straight to path.resolve() + fs.mkdir() with no containment check at all, and the slug_prefix argument — concatenated into the filename — carried a second traversal vector via /, ... Fixed in 2.0.3 with an opt-in NOTEBOOKLM_VAULT_ROOT env var plus always-on slug_prefix sanitization — which means the patched default deployment is still containment-free unless the operator sets the env var; version-bump ≠ containment-enabled. Operator rules this adds to the MCP file-tool sweep: (1) test the directory argument and the filename-component argument separately — a sanitized filename says nothing about the directory, and vice versa; (2) a prompt-injected LLM driving the MCP is a realistic attacker path, so file-write tools double as a plant primitive (autostart dirs, shell startup files) even when written content is inert; (3) check whether the fix is opt-in config before crediting it. Lab-bounded: disposable MCP server, marker .md/.json files inside a temp canary directory only — never shell startup files or real config paths.

September 22 evening follow-up: agent CLI auto-spawns repo-controlled MCP config before any trust decision (Kimi Code)

  • Moonshot AI Kimi Code ≤ 0.31.0 MCP configuration loader (CVE-2026-95660, GHSA-jp7p-97jq-v89h, exploit public): agent-core-v2/src/agent/mcp/config-loader.ts auto-spawned servers declared in an untrusted workspace's .mcp.json — the repo-trust failure already cataloged for Claude Code Action — and the 0.31.1 fix reveals the second leg: it resolves fd/stty helpers to absolute paths "so untrusted workspaces cannot plant bare-name executables before confirmation." Two axes for every agent/IDE MCP-loader audit: (1) does opening a repo spawn anything from repo-controlled config before the trust prompt (spawn-on-open vs confirm-then-spawn); (2) the confirmation flow itself is attack surface — helper binaries invoked during setup by bare name are plantable via repo-local PATH/env manipulation, so test helper resolution with a scratch PATH dir you control before crediting any "asks first" mitigation. Lab-bounded: disposable repo with an inert marker MCP server + a fake fd in a scratch PATH; never point an agent at untrusted repos with real credentials loaded.

October 2 follow-up: the reference fetch tool itself is SSRF, exploited publicly, fix unmerged (mcp-server-fetch)

  • modelcontextprotocol/mcp-server-fetch and mcp-server-everything through 2026.6.4 fetch_url (CVE-2026-104120, GHSA-v78q-44mm-wf7q, exploit public, fix PR awaits acceptance): the url argument of the Fetch tool is fetched without destination confinement — the canonical, most-deployed reference MCP server ships an unguarded URL sink. Three operator rules: (1) when a tool is the reference implementation, everyone copied its gaps — auditing one deployment of mcp-server-fetch style code tells you the shape of hundreds of bespoke fetch tools; (2) "published CVE" ≠ "fixed": the advisory states the fix PR is unmerged, so version pinning is worthless here — test the live deployment, not the version string, and check upstream PR state before crediting a patch window; (3) in an agent bundle, the fetch tool is a prompt-injection-reachable SSRF primitive by design (the model chooses the URL) — prove with an owned callback that the tool follows redirects/file:-shaped/internal ranges as configured, and report guard coverage (or absence) of the final destination, not just the initial URL. Lab-bounded: owned callback endpoints + synthetic internal canaries only.

October 5 follow-up: documented restriction guards only the documented argument form (k6 MCP server)

  • Grafana k6 MCP server convert_playwright_script arbitrary file read (CVE-2026-89039, GHSA-ffg3-vfrj-4mjj, medium, v0.3.0+): the prompt's playwright_script argument is cwd-restricted only in the documented @-prefixed form; a bare path is resolved by a separate undocumented code path with no restriction at all, and a leading ~ is expanded to the server user's home — so ~/.ssh/id_ed25519-shaped arguments are directly addressable and file contents return in the prompt response. The cwd restriction, even where applied, is bypassable via a symlink planted inside the cwd because the path is never canonicalized before the check. v0.3.0/v0.4.0 apply no restriction to either form.
  • Operator rules for every MCP/file-reading tool: (1) enumerate argument forms, not just values — a guard on the documented syntax (@file, --from-file, URL-scheme form) routinely sits beside an undocumented fallback branch that skips it; call the tool with every plausible input shape (bare path, ~-prefixed, scheme-prefixed, relative/absolute) and compare restriction behavior; (2) the shipped docs/description is the map of checked paths — the unchecked path is whatever the docs never mention, so grep the handler for the second resolver branch; (3) symlink-inside-root is the standing no-canonicalize companion test (pair with the Sept 21 ACE-MCP caller-supplied-root rule: even when the root is server-fixed, test symlink escape). Lab-bounded: disposable MCP server + synthetic canary files outside the cwd; never read real key material — request a marker file you planted in /tmp as the proof.