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:
- Local MCP/dashboard listeners are browser-reachable. Three advisories show the same shape — a tool server bound locally or on
0.0.0.0with no Host/Origin validation, so DNS rebinding preserves an attacker-controlledHost/Originwhile routing browser requests to the victim's localhost listener (@zereight/mcp-gitlab,mysql-mcp-serverSSE, Mockoon admin API with wildcard CORS). - 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.
- 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 (
__sanitizerebinding), and code printers calling globalJSON.stringifyat 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.
- Local-listener rebinding matrix. For each locally-bound tool server: enumerate binds (
127.0.0.1vs0.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. - 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. - 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.
- Wildcard-subject check (owned IdP + owned accounts only): assert subjects containing
%/_and confirm lookup semantics; SQLite vs Postgres path differences matter. - Reserved-IP SSRF class list. When testing URL screens, include the reserved-but-not-private classes (e.g.
168.63.129.16on Azure VMs, cloud metadata equivalents you are explicitly authorized to probe); stop at response-disclosure evidence. - 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.transportis omitted, SSE is the default transport, configured with only host+port — no auth, Host, Origin, or caller-secret field exists. TheX-PC-Token-Keymiddleware 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.internallateral movement (CVE-2026-58197, GHSA-qg2g-g9w3-m5h8, CVSS 8.8): the defaultnetworkpermission profile isinsecure_allow_all: true, so any containerized MCP serverthv rununder defaults can POST tohttp://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: disposablemcp/filesystemcontainer + synthetic canary files.
September 20 follow-up: nexus-mcp reauth tool builds a shell string from the url argument¶
nexus-mcpnexus_reauthMCP tool command injection (CVE-2026-94031, GHSA-c2f2-x95m-p9vw, exploit public): the auth helper (src/auth/browser.ts) passes the caller-suppliedurlargument straight into achild_process.execstring — 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 forexec/execSync/os.systemand 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_snippetpath traversal (CVE-2026-94046, GHSA-8wj2-8g8v-vcxr, exploit public):isPathInsideProjectRootonly blocksfilePathfrom escaping the caller-suppliedprojectRootPath; the root itself is untrusted client input — setprojectRootPath=/etc,filePath=hostsand 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_filewritescontentto an unsandboxedfilePath(CVE-2026-94044, 7.3, GHSA-wvg2-xpp5-j725); MCPHub ≤ 1.0.32importTemplateimproper-privilege drift on the template-import endpoint (CVE-2026-94047, 6.3, GHSA-2xcr-p42x-hfx6); cowork_benchpdf-tools-mcppdf_file_pathSSRF (CVE-2026-94051, GHSA-6j48-ggfg-8xw2) — a PDF-tool path/file argument is a fetch/file sink: test it against an owned callback andfile://-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-mcpvault_batch/POST /batch-to-vaultarbitrary file write (CVE-2026-61647, GHSA-jjhp-8crj-mppq, high): thevault_dirtool argument went straight topath.resolve()+fs.mkdir()with no containment check at all, and theslug_prefixargument — concatenated into the filename — carried a second traversal vector via/,... Fixed in 2.0.3 with an opt-inNOTEBOOKLM_VAULT_ROOTenv var plus always-onslug_prefixsanitization — 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/.jsonfiles 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.tsauto-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 resolvesfd/sttyhelpers 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 fakefdin 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-fetchandmcp-server-everythingthrough 2026.6.4fetch_url(CVE-2026-104120, GHSA-v78q-44mm-wf7q, exploit public, fix PR awaits acceptance): theurlargument 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_scriptarbitrary file read (CVE-2026-89039, GHSA-ffg3-vfrj-4mjj, medium, v0.3.0+): the prompt'splaywright_scriptargument 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/tmpas the proof.