Security Considerations
BEST is an open protocol with a broad attack surface. This page catalogues the security requirements that every conformant implementation must observe.
TLS
All BEST HTTP endpoints MUST be served over HTTPS. Cleartext HTTP MUST NOT be used in production. Any MCP transport carrying BEST traffic or credentials MUST provide confidentiality and integrity equivalent to TLS 1.2 or higher. Clients and servers MUST validate certificates and hostnames and MUST NOT send credentials over insecure or unauthenticated transports.
Authentication and Authorisation
Only GET /.well-known/best is unauthenticated by design — it is the public discovery endpoint. Every other endpoint MUST require authentication.
Implementations SHOULD define distinct authorisation scopes or roles:
| Scope | Applies to |
|---|---|
| Read | GET /events, GET /commands, GET /commands/{schema}/{version}, GET /queries |
| Write | POST /commands, POST /subscriptions, DELETE /subscriptions/{id} |
GET /events MUST require authentication and tenant-scoped authorisation unless a specific event stream is explicitly designated public. Unauthenticated callers MUST NOT be able to read domain events.
Command Authorisation
The scopes above are necessary but not sufficient. POST /commands is a single endpoint through which every named operation a service accepts is invoked, and those operations do not share a blast radius — submitting a timesheet and erasing a person's data arrive at the same URL under the same Write scope. A credential that may send one command MUST NOT thereby be able to send all of them.
Per-command policy
- Servers SHOULD maintain an explicit policy of which command types each credential, service, or role may submit, and evaluate it on every submission.
- That policy MUST be deny-by-default: a command type with no entry is denied. Absence of a rule is not permission, and an empty policy MUST NOT mean "allow everything".
- Policy MUST be scoped to the deployment it governs. A policy shared across environments grants production access as a side effect of enabling a command in test.
GET /commandsSHOULD return the commands the authenticated caller may actually submit. A catalogue advertising commands the caller will be denied causes agents to plan operations that cannot complete, and is indistinguishable to them from a broken service.- A denial SHOULD be reported distinguishably from an unknown command type, so an operator can tell a missing policy entry from a missing schema. Where catalogue membership is itself confidential, this MAY be collapsed into a single response.
Actor binding
Command payloads routinely carry fields naming a person or subject — an approver, creator, sender, owner, tenant. These are caller-supplied and carry no more authority than source does.
- Servers MUST NOT treat an actor field in
dataas authenticated identity. - Where a field identifies the acting principal, the server MUST either reject a payload whose value differs from the authenticated principal, or overwrite it with the verified value before dispatch.
- Overwriting SHOULD be preferred to demanding an echo. A field whose only acceptable value is already determined by the credential is pure failure surface: a caller can only restate it correctly or receive a confusing rejection, and callers frequently do not know their own internal identifiers. Stamping cannot change the outcome for a caller that sent the right value, and converts a guaranteed rejection into success for one that did not.
- Where a field identifies a subject other than the caller — whose invoice, whose engagement, whose organisation — the server MUST authorise the caller's access to that subject. Such a field is a reference, and an unchecked reference is an insecure direct object reference.
- Authorisation MUST NOT be inferred from the payload validating. Schema validation establishes shape, never permission.
High-impact commands
- Commands that are destructive, irreversible, or that bypass a compliance control SHOULD require a control beyond the submitting credential: human approval, a second principal, or an out-of-band confirmation.
- Such a control MUST NOT be self-serviceable. If the approval step can be performed by the same credential that submitted the command, it is an audit trail, not a control.
- Where a server offers an approval mode that only logs what it would otherwise reject, that mode MUST NOT be the default, and its active mode MUST be discoverable by operators.
Credential Passthrough at Intermediaries
An intermediary that sits between end callers and a BEST endpoint (an MCP server, a gateway, a multi-user chat backend) may need to forward each caller's own credential upstream instead of authenticating with a single fixed identity. Per-caller credentials are preferable to a shared key — the BEST service can then apply per-user authorisation and audit — but forwarding creates a credential-leakage risk that implementations must control:
- Passthrough MUST be opt-in per upstream connection, disabled by default. An inbound
Authorizationheader may carry a credential intended for the intermediary itself (e.g. OAuth between an MCP client and an MCP server); forwarding it by default would leak that credential across a trust boundary. The operator enabling passthrough is asserting that the caller's token and the upstream BEST credential belong to the same trust domain. - A forwarded credential MUST be sent only to the connection's configured endpoint — never to a URL derived from request content — and only over transports satisfying the TLS requirements.
- Where an explicit per-request credential (e.g. an
X-Api-Keyoverride) and an ambientAuthorizationheader are both present, the explicit credential SHOULD take precedence. This mirrors dual-auth BEST gates, where a present API key is authoritative and never falls through to a bearer token. - Intermediaries MUST NOT log or persist forwarded credentials. Diagnostics (e.g. warning that a bearer token was ignored because passthrough is disabled) MUST omit the credential value.
- Multi-user intermediaries SHOULD fail closed: configure the connection's static credential as a deliberately invalid placeholder so a request that arrives without per-caller credentials is rejected by the BEST service rather than silently executing under a shared identity.
The reference MCP server (@behavioralstate/best-mcp) implements this contract via its allowBearerPassthrough connection setting — see the mcp-server README.
Command Ingestion — Schema Selection
The dataschema field in an inbound command is a selector, not a location. It names an entry the server already owns and serves at GET /commands/{schema}/{version}, and a conformant client takes its value verbatim from the command catalogue. The security property that matters is that the validating schema comes from the server's own catalogue — not which server-owned identifier keys that lookup.
A server that fetches the caller-supplied dataschema URI to perform validation is architecturally incorrect: the server owns its schema catalogue and does not need the client to point it to a schema. It is also a security risk: a caller can supply an internal URI — a cloud metadata service (http://169.254.169.254), an internal database, or a private host — and the server becomes an unwitting proxy. This is a Server-Side Request Forgery (SSRF) attack.
Requirements:
- Servers MUST select the schema for validation from their own catalogue. The lookup MAY be keyed by the command
type, or by the schema name carried indataschema, provided the entry selected is one the server owns. - Servers MUST NOT fetch the caller-supplied
dataschemaURI for any purpose. - Servers MUST reject a command whose schema cannot be resolved to a catalogue entry. Forwarding an unvalidated payload is not an acceptable fallback — a resolution miss must fail closed.
- Servers MUST document which version policy they apply. A server that honours a caller-declared version MUST reject a version that does not exist rather than silently substituting another; a server that always selects the latest MUST ignore the caller's declared version rather than partially honouring it.
- Servers SHOULD reject a command whose
dataschemadoes not match a known entry inGET /commands, even when the schema was selected bytype. - If a server does support fetching remote schemas (for example, for cross-service federation), it MUST restrict URI schemes to HTTPS, apply a strict allowlist, disable redirects, and enforce fetch size, depth, and timeout limits.
One identifier, or a total mapping
type (PascalCase) and the catalogue's schema (kebab-case) are distinct fields naming the same operation. Servers commonly derive one from the other by string transformation, and that derivation is where authorisation and validation drift apart.
Requirements:
- Schema selection, authorisation, and dispatch MUST be keyed on the same identifier. A server that authorises a command under one identifier while validating or dispatching it under another can be induced to authorise operation A and execute operation B — a confused-deputy vulnerability.
- Where a server derives one identifier from the other, that transformation MUST be total and unambiguous over its catalogue. If it is not — because some names do not round-trip, or two entries collapse onto one key — the server MUST resolve the identifier once and use the single resolved value for every subsequent decision.
- Where
typeanddataschemadisagree about which operation is being invoked, servers MUST NOT satisfy one decision from each. Reject the command, or resolve consistently from one field and log the divergence.
Why this is worth stating. A lossy
type→schematransformation fails in two directions. If the authorisation key misses, a correctly permitted command is denied with a policy error naming an operation the operator never configured — expensive to diagnose, but safe. If the validation key misses while authorisation succeeds, the caller has selected their own contract. Deny-by-default policy (see Command Authorisation) keeps the second case from being reachable in most implementations, but it should not be the only thing standing in the way.
Command Replay Protection
CloudEvent id is a UUID and MUST be treated as an idempotency key by servers.
Requirements:
- Servers MUST detect and reject or safely ignore duplicate command submissions (same
id, same authenticated source) within a defined retention window appropriate to the domain. - If a previously seen
idis reused with different payload contents, the server MUST reject it as invalid (409 Conflict). - Replay detection scope MUST include the authenticated tenant and sender identity, not only the
idstring, to prevent cross-tenant or cross-source collisions.
CloudEvent source Field
The source field in a command is caller-declared and MUST NOT be treated as authenticated sender identity.
Requirements:
- Servers MUST NOT grant elevated permissions, apply authorisation policies, or make security decisions based on a caller-supplied
sourcevalue. - If
sourceis used for audit or observability, the server SHOULD either overwrite it with the verified principal identity (derived from the authenticated session) or record both the declared and verified values. - If a mismatch between
sourceand the authenticated principal is a policy violation, the server MUST reject the command.
Webhook SSRF Protection
When a subscription is registered with a webhook.url, the server will subsequently POST CloudEvents to that URL. A caller can exploit this to make the server issue requests to internal services.
Requirements:
- Servers MUST validate
webhook.urlbefore storing or using it. - Servers MUST reject URLs that resolve to loopback addresses, link-local addresses (
169.254.0.0/16), RFC 1918 private ranges (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), multicast addresses, or internal DNS names. webhook.urlMUST be an absolute HTTPS URL and MUST NOT contain userinfo credentials in the URL.- Webhook delivery MUST NOT follow HTTP redirects, or MUST re-apply the same validation rules to any redirect target before following.
- Servers MUST re-validate the resolved IP address at delivery time, not only at registration time, to prevent DNS rebinding attacks.
- Servers SHOULD require proof of webhook endpoint ownership before activating delivery (for example, a challenge-response handshake or an out-of-band confirmation).
Multi-Tenant Isolation
For implementations that serve multiple tenants:
- Access to tenant-scoped manifests and resources MUST be authorised per tenant. Knowing or guessing another tenant's identifier MUST NOT grant access to their resources.
- Tenant context MUST be derived from the authenticated identity (Bearer token, API key), not from caller-supplied path parameters, query strings, body fields, or CloudEvent attributes.
- Tenant-specific HTTP responses MUST be isolated in caches (
Varyheaders or non-cacheable) and MUST NOT be served to a different tenant. - Command deduplication, event streams, and webhook configurations MUST be isolated per tenant.
Input Limits
Servers MUST enforce maximum request body sizes for POST /commands and POST /subscriptions and SHOULD return 413 Payload Too Large when exceeded. Servers MUST bound JSON nesting depth, collection sizes, string lengths, and total attribute counts to prevent parser or validator resource exhaustion.
Servers SHOULD apply rate limits and quotas per authenticated client and per tenant for command submission and subscription registration.
Manifest Content
The /.well-known/best manifest is publicly accessible by design. Its content MUST be limited to information intended for unauthenticated disclosure:
- Service descriptions, capability names, and transport endpoints — yes
- Internal-only endpoint addresses, tenant metadata, credential hints, or sensitive integration names — MUST NOT appear
The http.endpoint value MUST be a consumer-facing public URL. Internal service mesh addresses, private VPC hostnames, and backend-only paths MUST NOT appear in the manifest.