Skip to content

environmentd: allow widening CORS allowlist via system variable#37621

Draft
jubrad wants to merge 1 commit into
MaterializeInc:mainfrom
jubrad:http-dynamic-cors-origins
Draft

environmentd: allow widening CORS allowlist via system variable#37621
jubrad wants to merge 1 commit into
MaterializeInc:mainfrom
jubrad:http-dynamic-cors-origins

Conversation

@jubrad

@jubrad jubrad commented Jul 13, 2026

Copy link
Copy Markdown
Member

Motivation

The HTTP API CORS allowlist is fixed at startup via --cors-allowed-origin. Adding an origin (e.g. a new console deployment or a customer-facing tool) requires an environment restart. This adds a way to extend the allowlist at runtime.

Description

Adds the http_additional_cors_allowed_origins system variable (comma-separated list, default empty), settable by superusers:

ALTER SYSTEM SET http_additional_cors_allowed_origins = 'https://foo.example, *.staging.example.com';

Takes effect on the next request, no restart. Entries use the same syntax as the CLI flag: exact origin, wildcard subdomain (*.example.com), or bare *.

The variable is checked as a union with the startup list, so it can only widen the allowlist. Origins provided via --cors-allowed-origin (needed to manage Materialize) can never be removed or overridden through the variable.

Implementation, following the max_connections plumbing pattern:

  • HTTP_ADDITIONAL_CORS_ALLOWED_ORIGINS is a regular VarDefinition (user visible, added to user_modifiable like network_policy). A CorsOriginList domain constraint rejects entries that are not valid header values at SET time.
  • The coordinator registers a system variable callback (like connection_limit_callback) that hands the raw value to environmentd via the new mz_adapter::Config::additional_cors_origins_callback. environmentd parses it and writes the origins into a shared Arc<RwLock<Vec<HeaderValue>>>.
  • AllowedOrigins in environmentd::http holds the startup list plus that shared handle and backs an AllowOrigin::predicate for both CorsLayers (base router and MCP router).
  • The MCP server-side Origin check (DNS rebinding defense) honors the same union.
  • The prebuilt AllowOrigin is dropped from ServeConfig/HttpConfig, the raw origin list is now the single source of truth. New mz_http_util::parse_origin_list helper.

One minor behavior change: a bare * in the CLI list previously produced AllowOrigin::any() (a literal * response header), it now mirrors the request origin. Equivalent for browsers.

Tests: test_parse_origin_list (mz-http-util), cors_origins_var_user_settable (mz-sql, superuser can see and modify the var), extended test_validate_origin (mz-environmentd, origins added through the shared handle take effect dynamically). SHOW ALL output in test/testdrive/session.td updated for the new user-visible variable.

Tips for reviewer

CORS preflights always return 200, denial is the absence of the access-control-allow-origin response header. CORS is a browser-side control, the only server-side origin rejection (403) is on the MCP endpoints.

Checklist

  • This PR has adequate test coverage / QA involvement has been duly considered. (trigger-ci for additional test/nightly runs)
  • This PR has an associated up-to-date design doc, is a design doc (template), or is sufficiently small to not require a design.
  • If this PR evolves an existing $T ⇔ Proto$T mapping (possibly in a backwards-incompatible way), then it is tagged with a T-proto label.
  • If this PR will require changes to cloud orchestration or tests, there is a companion cloud PR to account for those changes that is tagged with the release-blocker label (example).
  • If this PR includes major user-facing behavior changes, I have pinged the relevant PM to schedule a changelog post.

🤖 Generated with Claude Code

@jubrad jubrad force-pushed the http-dynamic-cors-origins branch from 28180a8 to cb12ac4 Compare July 13, 2026 19:44
Add the http_additional_cors_allowed_origins system variable
(superuser settable). Origins listed there (comma-separated, same
syntax as --cors-allowed-origin) are allowed for CORS in addition to
the origins provided at startup. The variable can only widen the
allowlist: startup origins can never be removed through it.

The variable value reaches the HTTP servers the same way connection
limits do: the coordinator registers a system variable callback that
writes the parsed origins into a shared handle the CORS predicate
reads on every origin check, so ALTER SYSTEM SET takes effect without
a restart. A domain constraint rejects origins that are not valid
header values at SET time.

The MCP server-side Origin check (DNS rebinding defense) honors the
same union.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jubrad jubrad force-pushed the http-dynamic-cors-origins branch from cb12ac4 to 2e60a7f Compare July 13, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant