Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt

Use this file to discover all available pages before exploring further.

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 execute_action traverses five gates. Removing any of them requires editing the plugin source:
MCP client

   ▼  ① Inbound auth (RFC 8707-validated JWT from the MCP transport)
SkilledOpenApi meta-tools

   ▼  ② Bundle origin (signature verified, see below)
HiddenOpRegistry

   ▼  ③ Per-skill ABAC (action.requiredAuthorities via AuthoritiesEngine)

   ▼  ④ Credential scoping (vaultRef must resolve via the configured CredentialResolver)
OpenApiHttpExecutor

   ▼  ⑤ Outbound execution (SSRF allowlist + IP blocklist + circuit breaker)
Customer REST API

Bundle signing (OPA model)

The integrity envelope on every bundle is a JWT-of-hashes modeled on OPA’s signed-bundle pattern.
interface BundleIntegrity {
  alg: 'RS256' | 'EdDSA';
  keyId: string;
  signature: string;   // base64url
  digest: string;      // sha256 hex of canonical bundle bytes (excluding integrity)
}
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.
SkilledOpenApiPlugin.init({
  // ...
  requireSignature: true,                  // default: true. Never set false in production.
  trustedKeys: [
    {
      keyId: 'frontmcp-cloud-2026-01',
      alg: 'RS256',
      publicKeyPem: process.env.FRONTMCP_BUNDLE_PUBKEY!,
    },
  ],
})
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:
{
  "operationId": "refundInvoice",
  "requiredAuthorities": {
    "operator": "AND",
    "permissions": { "all": ["invoices:write"] },
    "attributes": { "match": { "input.amount": { "$lte": 10000 } } }
  }
}
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 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

OWASP MCP riskThis plugin’s defense
MCP-1 Tool poisoningBundle signing + bundle-diff log on every swap (rug-pull detection)
MCP-2 Prompt injection (direct)Inbound auth + meta-tool input schema strict validation
MCP-3 Indirect prompt injectionStructural output envelope from execute_action; output schema enforcement
MCP-4 Excessive agencyPer-skill ABAC + credential scoping + structured ABAC denial path
MCP-5 Sensitive info disclosureOutbound allowlist + audit log + no credential echo in logs/traces
MCP-6 Insecure tool descriptionSigned-bundle origin + bundle-diff log surfaces description changes
MCP-7 Confused deputyRFC 8707 enforcement on every JWT (push channel + caller-token passthrough)
MCP-8 Supply chainSigstore/SLSA provenance verification on npm-source (planned v1.2.x)
MCP-9 Excessive permissionsPer-bundle credential allowlist + per-skill scoping
MCP-10 Insufficient observabilityOTel spans + audit log + bundle diff + /healthz enrichment

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_action returns { ok, status, data, contentType, error }data is 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 via op.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.
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
  • /healthz polling configured to alert on lastPullAt > 2 * pollIntervalMs (saas source) or lastApplyError != 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)
Track the v1.2.x roadmap for release dates.