- 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
Everyexecute_action traverses five gates. Removing any of them requires editing the plugin source:
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 inside execute_action, with the caller’s authInfo from the MCP request mapped through AuthoritiesContextBuilder. Denial returns ok: false with a structured reason and is recorded in the audit log.
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:
execute_actionreturns{ ok, status, data, contentType, error }—datais opaque to the LLM unless explicitly extracted. - 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). - Optional content sanitizer hook (planned v1.2.x): customers register a
sanitize(action, data)callback to strip HTML, redact PII, or refuse content matching known-injection heuristics.
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
-
/healthzpolling configured to alert onlastPullAt > 2 * pollIntervalMs(saas source) orlastApplyError != null
Deferred to v1.2.x
The following are planned but not in v1.2.0:- Sigstore Cosign v3 keyless verification (alongside the JWT/key path)
- GitHub artifact attestation verification for npm-source
- Webhook channel (
POST /__skilled_openapi/push) with nonce + replay window - Optional content-sanitizer hook for response bodies
- Discovery-bundle pattern for signing key rotation
- Bundle rollback admin endpoint (
POST /__skilled_openapi/rollback) - Per-service circuit-breaker (currently relies on per-host concurrency cap)
- TLS pinning per service (opt-in)