[BREAKING] Python: DevUI: tighten default access controls and CORS posture#5740
Conversation
Adjusts the default configuration of the DevUI server so the out-of-the-box posture matches what most callers expect when running locally. Adds explicit opt-outs for callers who need the previous behavior. - DevServer gains auth_enabled and auth_token constructor params; auth is on by default. Auto-generates and logs a token when none provided. - CORS default is an empty allowlist on every host. Callers wanting cross-origin pass cors_origins explicitly. - Streaming /v1/responses no longer sets Access-Control-Allow-Origin directly; CORSMiddleware owns all CORS decisions. - Loopback binds enforce a Host-header allowlist. - /meta moved out of the auth bypass list (was alongside /health and /). - serve() default flipped to auth_enabled=True; passes auth args through to DevServer instead of using env-var indirection. - CLI: --auth opt-in replaced with --no-auth opt-out; --auth-token preserved. - Tests cover the eight behaviors above in test_server.py.
There was a problem hiding this comment.
Pull request overview
Tightens the Python DevUI server’s default security posture by enabling Bearer-token auth by default, removing permissive default CORS behavior, and adding a loopback Host-header allowlist to mitigate DNS rebinding—while providing explicit opt-outs for testing/embedded scenarios.
Changes:
- DevServer now supports
auth_enabled(defaultTrue) andauth_token(env var fallback, else auto-generated) with constant-time token comparison. - CORS defaults to an empty allowlist; streaming responses no longer hardcode
Access-Control-Allow-Origin. - CLI and
serve()defaults flip to auth-on-by-default (--no-authopt-out) and tests are expanded/updated accordingly.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| python/packages/devui/agent_framework_devui/_server.py | Implements new default auth, tighter CORS defaults, loopback Host-header allowlist, and removes hardcoded ACAO on streaming responses. |
| python/packages/devui/agent_framework_devui/init.py | Updates serve() defaults to auth-enabled and adjusts token handling / safety checks. |
| python/packages/devui/agent_framework_devui/_cli.py | Replaces --auth with --no-auth and wires auth args through to serve(). |
| python/packages/devui/tests/devui/test_server.py | Adds new tests for auth/CORS/host-header/streaming behavior and updates existing tests for new defaults. |
| python/packages/devui/tests/devui/test_ui_memory_regression.py | Disables auth for the memory regression server fixture to keep the test focused. |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 94%
✓ Correctness
The PR correctly tightens CORS defaults, auth posture, and adds DNS-rebinding protection. Two correctness issues found: (1) the
/metaendpoint'sauth_requiredresponse field still reads from theDEVUI_AUTH_TOKENenv var, but the PR removes the code inserve()that previously set that env var — so the field will reportFalseeven when auth is active; (2) the Host-header middleware's port-stripping logic (split(':', 1)[0]) breaks for IPv6 addresses like[::1], despite_LOOPBACK_HOSTSexplicitly including them.
✓ Security Reliability
The PR correctly tightens CORS, auth defaults, and adds DNS-rebinding protection. However, the
/metaendpoint'sauth_requiredfield (line 445 of_server.py) still readsos.getenv("DEVUI_AUTH_TOKEN")to determine whether auth is active. The oldserve()wrote that env var (line 173 of__init__.py), but this PR removes that write.DevServer._resolve_auth_token()also reads but never writes the env var. As a result,/metawill reportauth_required=Falsewhenever the token is auto-generated or passed via theauth_token=parameter — exactly the default localhost path. This could mislead UI clients about the security posture.
✓ Test Coverage
The PR adds eight well-structured security tests covering the core posture changes (default auth, CORS tightening, host-header allowlist, CLI flag flip). However,
test_streaming_response_does_not_hardcode_acao_headeris vacuous because auth is enabled by default and the request carries no Bearer token, so auth middleware returns 401 before the streaming handler runs — the assertion passes trivially. Two additional behavioral paths lack coverage: rejection of an incorrect Bearer token (tests only cover missing token and valid token), and theDEVUI_AUTH_TOKENenv-var fallback in_resolve_auth_token(all tests either supply an explicit token or clear the env var).
✓ Design Approach
I found two design-level gaps in the security hardening. First, the new Host-header allowlist only recognizes four literal host strings, so valid loopback binds like
127.0.0.2lose the DNS-rebinding protection this PR is trying to add. Second, auth configuration was moved ontoDevServer, but/metastill reportsauth_requiredfromDEVUI_AUTH_TOKEN, so directDevServer(auth_token=...)orserve(auth_token=...)instances can require auth while advertising that auth is off.
Automated review by moonbox3's agents
- /meta now derives auth_required from self.auth_enabled instead of reading DEVUI_AUTH_TOKEN, so the auto-generated and explicit auth_token paths report correctly. - Reorder middleware so the loopback Host-header allowlist is registered last; Starlette wraps later-added middleware around earlier-added ones, so the host check now runs outermost (before CORS/auth) as intended. - Rework comments to describe the behavior rather than threat scenarios. - Streaming-headers and CORS tests now construct the server with an explicit auth_token and send a Bearer header, so the assertions actually exercise the streaming/CORS path instead of short-circuiting in the auth middleware.
Motivation and Context
The DevUI server's out-of-the-box defaults around CORS and auth were looser than most callers expect when running locally. This PR adjusts the defaults so a bare
devui ./agentsinvocation matches the typical mental model (loopback, same-origin, gated), while keeping clean opt-outs for the rare cases that need the old behavior.Description
DevServernow takesauth_enabled(defaultTrue) andauth_token(defaults to theDEVUI_AUTH_TOKENenv var, then an auto-generated value logged at startup). The Bearer-token middleware uses constant-time comparison.CORS defaults to an empty allowlist on every host; callers wanting cross-origin pass
cors_origins=[...]explicitly. Two streaming/v1/responsespaths that previously setAccess-Control-Allow-Originthemselves no longer do so —CORSMiddlewareowns all CORS decisions.A small Host-header allowlist middleware runs ahead of CORS on loopback binds, rejecting requests addressed to non-loopback names.
/metawas moved out of the auth bypass list (it had been alongside/healthand/).serve()flips its default toauth_enabled=Trueand passes auth args through toDevServerdirectly, replacing the previous env-var indirection. The production-environment refusal (network bind + no explicit token) is preserved.CLI:
--authopt-in is replaced with--no-authopt-out;--auth-tokenis unchanged.Eight new tests in
test_server.pycover: streaming response headers, CORS default, auth-on-by-default,auth_enabled=Falseopt-out, valid token acceptance,/metagating, Host-header allowlist, andserve()default. Two existing tests were updated to passauth_enabled=Falseand a loopbackbase_urlsince they predate these defaults.Contribution Checklist