Skip to main content
The plugin sits across three threat-rich domains:
  1. MCP server security (OWASP MCP Top 10 2026, CVE-2025-6514, the Anthropic Git MCP CVE chain CVE-2025-68143/144/145)
  2. Indirect prompt injection (Anthropic’s Feb 2026 system card identifies indirect injection as the dominant attack class)
  3. Outbound HTTP from a server-side runtime (SSRF surged 452% from 2023 to 2024)
Every security control in this page maps to one of those vectors. This is not a theoretical surface — it’s a defense-in-depth stack for the production pattern of “wrap a customer’s REST API and serve it to an LLM.”

Five-gate authorization stack

Every callTool(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:
The enclave itself is a sixth, ambient containment layer: the dependency-free @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)

The integrity envelope on every bundle is a JWT-of-hashes modeled on OPA’s signed-bundle pattern.
Verification flow:
  1. Re-compute sha256 of the canonicalized bundle minus the integrity field.
  2. Reject if the computed digest doesn’t match integrity.digest.
  3. Look up the trusted public key by integrity.keyId. Reject if unknown.
  4. Reject if the trusted key’s algorithm doesn’t match integrity.alg.
  5. Verify the signature over the canonical bytes (not the digest hex) using the public key.
Any failure → keep the existing bundle, surface failure via /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 via BundlePushJwtVerifier:
  • aud must equal the configured expectedAudience (typically bundleId)
  • resource must equal the configured expectedResource (typically the FrontMCP server’s canonical URL)
  • iss must equal expectedIssuer
  • roles must include at least one of requiredRoles (default: ['frontmcp:cloud:push'])
This blocks the confused-deputy class of attack where a token issued for one customer’s FrontMCP gets replayed against another’s. For 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 in bundle.schema.ts enforces:
  • URL construction: WHATWG URL only, 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: additionalProperties constraints applied at the Zod boundary; the executor validates input against the schema again before invoking the upstream
  • No eval, Function, child_process, vm, or dynamic require anywhere in the executor or sync paths
  • No bundle data in shell commands — the plugin never shells out
The CVE-2025-6514 root cause was passing untrusted server response data into system handlers. The plugin doesn’t shell out at all.

SSRF defenses (layered)

A hostname allowlist alone is insufficient (DNS rebinding bypasses it). The plugin layers:
  1. URL string check before DNS resolution: reject file:, data:, gopher:, ftp:; require https: (allow http: only via explicit allowHttp: true).
  2. Hostname allowlist: only declared services[].baseUrl hosts.
  3. Cloud-metadata hostname blocklist: metadata.google.internal, metadata.azure.com, metadata.aws.com blocked even if technically allowlisted.
  4. 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)
  5. 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:
Evaluation runs through 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 callTool returns the action’s data into the sandboxed AgentScript, and only the script’s final return <value> (surfaced as run_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 via op.maxResponseBytes).
The signed-bundle origin reduces the surface (you trust your own CI pipeline to produce instructions), but it does not eliminate it. Treat customer REST API responses no more than user input. Document this loudly in your team’s playbook.

Production checklist

Before flipping to production:
  • dev: false (default) and requireSignature: true (default)
  • At least one entry in trustedKeys[]
  • Credentials wired via the auth vault instead of the in-memory MemoryCredentialResolver for any non-trivial deployment
  • allowHttp: false (default) unless your upstream is on localhost
  • allowPrivateNetworks: false (default) unless self-hosted on a private network
  • outbound.maxConcurrencyPerHost tuned 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)