Skip to content

Security: API is unauthenticated by default and derives tenant identity from a client-supplied header #63

Description

@haasonsaas

Summary

Out of the box the control plane is a fully open, tenant-spoofable API.

Evidence

crates/sandboxwich-api/src/main.rs:908-948 (auth_and_tenant). When neither SANDBOXWICH_API_TOKEN (shared_token) nor SANDBOXWICH_TENANT_TOKENS is configured, the final else branch trusts the caller-supplied x-sandboxwich-tenant header and serves the request unauthenticated:

} else {
    request.headers().get("x-sandboxwich-tenant")
        .and_then(|value| value.to_str().ok())
        .map(str::trim).filter(|t| !t.is_empty())
        .unwrap_or(&state.default_tenant_id).to_string()
};

database_url defaults to a local SQLite file and auto_migrate defaults true, so the default configuration is an open API where any client selects any tenant by header — bypassing every ensure_tenant check. Separately, in shared-token mode every authenticated caller is assigned default_tenant_id, so that mode has no tenant isolation; only tenant_tokens mode segregates tenants.

Why it matters

This is a total authn/authz bypass in the default deployment and a silent single-tenant collapse in shared-token mode. The README advertises multi-tenant boundaries.

Suggested fix

  • Fail closed: refuse to start serve (or reject all non-probe requests) when no auth is configured.
  • Never derive tenant from a client header in an authenticated deployment; bind tenant to the presented credential.
  • Document explicitly that shared-token mode is single-tenant only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions