- MCP server security (OWASP MCP Top 10 2026, CVE-2025-6514, the Anthropic Git MCP CVE chain CVE-2025-68143/144/145)
- Indirect prompt injection (Anthropic’s Feb 2026 system card identifies indirect injection as the dominant attack class)
- Outbound HTTP from a server-side runtime (SSRF surged 452% from 2023 to 2024)
Five-gate authorization stack
EverycallTool(actionId, input) a run_workflow script issues traverses five gates. The AgentScript runs inside the enclave sandbox; the gates fire on each callTool, not once per run_workflow. Removing any of them requires editing the plugin source:
@enclave-vm interpreter gives the script no host access and no network except
callTool, and caps a single workflow’s step count, tool-call count, and wall
time. A workflow can chain many callTools in one round-trip, and each one runs
the full five-gate path above.
Bundle signing (OPA model)
Theintegrity envelope on every bundle is a JWT-of-hashes modeled on OPA’s signed-bundle pattern.
- Re-compute sha256 of the canonicalized bundle minus the
integrityfield. - Reject if the computed digest doesn’t match
integrity.digest. - Look up the trusted public key by
integrity.keyId. Reject if unknown. - Reject if the trusted key’s algorithm doesn’t match
integrity.alg. - Verify the signature over the canonical bytes (not the digest hex) using the public key.
/healthz + structured error log + audit trail. Never partial apply.
dev: true bypasses signature verification with a loud startup warning. Never set it in production. Setting requireSignature: false without dev: true also emits a startup warning to call out the misconfiguration.
RFC 8707 enforcement
Per the MCP authorization spec (draft 2026-03-15), every JWT must carry RFC 8707 Resource Indicators. The plugin enforces this on the SaaS-push channel viaBundlePushJwtVerifier:
audmust equal the configuredexpectedAudience(typicallybundleId)resourcemust equal the configuredexpectedResource(typically the FrontMCP server’s canonical URL)issmust equalexpectedIssuerrolesmust include at least one ofrequiredRoles(default:['frontmcp:cloud:push'])
passthrough caller token outbound calls (an AuthBinding with passthroughCallerToken: true), the plugin verifies the caller’s JWT has a resource claim matching the customer REST API’s base URL before forwarding it.
Input sanitization (CVE-2025-6514 lesson)
Even after signature verification, every string in a bundle is treated as adversarial input. The Zod schema inbundle.schema.ts enforces:
- URL construction: WHATWG
URLonly, never string concat - Path templates: reject
.., backticks,$(,${, whitespace,?,# - Header names: must match RFC 7230 token grammar
- Header values: stripped of CR/LF before assignment
- JSON Schema input:
additionalPropertiesconstraints applied at the Zod boundary; the executor validates input against the schema again before invoking the upstream - No
eval,Function,child_process,vm, or dynamicrequireanywhere in the executor or sync paths - No bundle data in shell commands — the plugin never shells out
SSRF defenses (layered)
A hostname allowlist alone is insufficient (DNS rebinding bypasses it). The plugin layers:- URL string check before DNS resolution: reject
file:,data:,gopher:,ftp:; requirehttps:(allowhttp:only via explicitallowHttp: true). - Hostname allowlist: only declared
services[].baseUrlhosts. - Cloud-metadata hostname blocklist:
metadata.google.internal,metadata.azure.com,metadata.aws.comblocked even if technically allowlisted. - DNS resolve once at request start; reject if any resolved IP falls in deny ranges:
- RFC 1918 private (10/8, 172.16/12, 192.168/16)
- Link-local (169.254/16) — blocks AWS/GCP/Azure metadata services (169.254.169.254)
- Loopback (127/8, ::1)
- IPv6 ULA (fc00::/7) and link-local (fe80::/10)
- Per-host concurrency cap (
maxConcurrencyPerHost, default 10).
allowPrivateNetworks: true bypasses the IP blocklist for self-hosted scenarios where the customer’s REST API legitimately lives on a private network. Loud startup warning when set. Default is fail-closed.
ABAC integration
requiredAuthorities on a skill or operation is the same AuthoritiesPolicy shape @frontmcp/auth uses everywhere:
AuthoritiesEngine from @frontmcp/auth — RBAC, ABAC, ReBAC, custom evaluators, and combinators (anyOf, allOf, not) are all available. See the authorities docs for the full grammar.
The check happens on each callTool the run_workflow script issues, with the caller’s authInfo from the MCP request mapped through AuthoritiesContextBuilder. A denied action throws inside the script (failing that callTool, and the workflow unless the script catches it) and is recorded in the audit log via the authority-fail event.
OWASP MCP Top 10 (2026) coverage
Indirect prompt injection mitigations
The customer’s REST API can return content (CRM names, ticket bodies, user-supplied fields) that contains instructions targeting the LLM. Defenses:- Structural separation: upstream responses never reach the LLM directly. Each
callToolreturns the action’sdatainto the sandboxed AgentScript, and only the script’s finalreturn <value>(surfaced asrun_workflow’s{ success, value, error, stats }) is handed back to the LLM. Raw response bodies are opaque to the model unless the script explicitly extracts and returns them. - Output schema validation as a bottleneck: every response validated against the bundle’s declared
outputSchema. - Response size cap (
defaultMaxResponseBytes, default 256KB; per-op override viaop.maxResponseBytes).
Production checklist
Before flipping to production:-
dev: false(default) andrequireSignature: true(default) - At least one entry in
trustedKeys[] - Credentials wired via the auth vault instead of the in-memory
MemoryCredentialResolverfor any non-trivial deployment -
allowHttp: false(default) unless your upstream is onlocalhost -
allowPrivateNetworks: false(default) unless self-hosted on a private network -
outbound.maxConcurrencyPerHosttuned to match your upstream’s rate-limit budget - Audit log routed to a SIEM
- SaaS-side metrics monitored to detect a stalled bundle pipeline (saas source)