Skip to content

TenantMiddleware: the tenant header value is bound unvalidated, so an over-long or malformed header becomes a 500 on write #366

Description

@antosubash

Summary

TenantMiddleware binds whatever string arrives in the configured tenant header. Nothing checks its length or character set before it becomes current_tenant_id, so every MultiTenantMixin read filters on it and every write stamps it. tenant_id is VARCHAR(50), so a header longer than 50 characters makes the first stamped insert fail with a StringDataRightTruncation on Postgres, surfaced as an unhandled 500. Any other junk (control characters, whitespace, a 10 KB value) is accepted as a tenant name as well.

Observed

simple_module_hosting/middleware.py:191-196 (0.0.26):

if tenant_id is None and self.header:
    header_value = Headers(scope=scope).get(self.header)
    if header_value:
        tenant_id = header_value

request.state.tenant_id = tenant_id

if tenant_id is not None:
    token = current_tenant_id.set(tenant_id)

Reproduction: a host with SM_MULTI_TENANT=true and SM_TENANT_HEADER=X-Tenant-ID, any module whose table carries MultiTenantMixin, and an anonymous request that creates a row:

curl -X POST http://localhost:8000/api/<module>/... -H "X-Tenant-ID: $(python3 -c 'print("a"*60)')" ...
-> 500, sqlalchemy.exc.DBAPIError ... StringDataRightTruncationError: value too long for type character varying(50)

Expected

The middleware validates the header against the column's own contract (at most 50 characters, a printable identifier) and treats an invalid value as no tenant (or answers 400), instead of binding it.

Why a module cannot work around it

The header is consumed and bound before any module code runs. A module can re-check request.state.tenant_id and refuse the request (the records module does, with a ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,49}$ pattern), but the contextvar has already been set for the rest of the pipeline and every other adopter of the mixin has to repeat the check.

Proposed API

Validate in TenantMiddleware: reject values longer than MultiTenantMixin's max_length or containing characters outside a documented identifier set, and expose the pattern from simple_module_db so modules and the middleware agree on it.

Context

Found while making the records module (antosubash/smpy_modules#37) multi-tenant; design doc docs/plans/2026-09-23-records-multitenancy.md there, gap L16.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions