Skip to content

feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514)#4527

Merged
doudouOUC merged 3 commits into
QwenLM:daemon_mode_b_mainfrom
doudouOUC:feat/daemon-cors-allow-origin
May 26, 2026
Merged

feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514)#4527
doudouOUC merged 3 commits into
QwenLM:daemon_mode_b_mainfrom
doudouOUC:feat/daemon-cors-allow-origin

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

Summary

  • Replace the unconditional denyBrowserOriginCors 403-wall with a configurable allowlist when --allow-origin <pattern> is set, unblocking browser webui clients.
  • New conditional capability tag allow_origin advertised when the flag is set so SDK / webui clients can pre-flight before issuing a cross-origin request.
  • Closes T2.4 in the #4514 inventory ("one S-sized PR that unblocks the entire browser-webui surface").
  • Bit-for-bit backward compatibility when --allow-origin is unset — the install path is unchanged and denyBrowserOriginCors keeps its job.

What's added

Layer Surface
CLI flag --allow-origin <pattern> (repeatable). Each pattern is * (any origin) or a canonical URL origin (<scheme>://<host>[:<port>]).
Boot validation parseAllowOriginPatterns rejects malformed entries with InvalidAllowOriginPatternError naming the bad pattern + canonical form. * + no token = boot refuses, mirroring the --require-auth + no token boot-refusal.
Middleware allowOriginCors(patterns) installed in place of denyBrowserOriginCors when patterns are configured. Match → echo origin + standard CORS headers + 204 OPTIONS short-circuit. Unmatched → 403 with the same envelope as today's wall. Origin: null always rejected. Vary: Origin on both paths.
Capability tag allow_origin: { since: 'v1' } advertised conditionally via CONDITIONAL_SERVE_FEATURES, gated on a new allowOriginActive toggle. The configured pattern list is intentionally NOT echoed in /capabilities (would leak the trusted-origin set to unauthenticated readers).

Architecture

Single-middleware ownership of CORS policy. Layering the new middleware in addition to denyBrowserOriginCors does NOT work — a matched next() hits the existing wall which 403s anyway. The branch at install time:

if (opts.allowOrigins?.length) {
  app.use(allowOriginCors(parseAllowOriginPatterns(opts.allowOrigins)));
} else {
  app.use(denyBrowserOriginCors);  // today's behavior preserved
}

The demo self-origin shim (server.ts near cachedSelfOrigins) runs BEFORE either CORS middleware and strips Origin for loopback self-hits, so it works regardless of --allow-origin configuration.

OPTIONS preflight short-circuits with 204 — standard CORS pattern. Safe because the actual subsequent request still runs the full chain (hostAllowlistbearerAuth → routes), so anti-DNS-rebinding and bearer enforcement still fire before any state read or mutation.

Access-Control-Allow-Origin echoes the request's origin verbatim (not literal *, even under the * pattern) — paired with Vary: Origin, this is what every browser cache expects, and it leaves room to add Access-Control-Allow-Credentials in a future flag without a schema change.

Access-Control-Allow-Credentials is intentionally NOT set today: bearer-token-via-Authorization works cross-origin without credentials: 'include'. Adding credentials would need a separate flag plus a "no * allowed" boot check (CORS spec forbids * with credentials).

Test plan

  • packages/cli/src/serve/auth.test.ts (+25 tests): parseAllowOriginPatterns happy + every reject branch (mixed-case host, trailing slash, path, userinfo, non-URL, empty hostname, malformed entry naming); allowOriginCors no-Origin pass-through, matched + CORS headers, OPTIONS 204, case-insensitive matching, unmatched 403 + Vary: Origin, * echo, * + Origin: null rejection.
  • packages/cli/src/serve/server.test.ts (+10 route tests): matched origin → 200 + CORS headers; OPTIONS preflight; unmatched origin still 403 with no leaked CORS headers; CLI/SDK clients (no Origin) pass; capability tag advertised on/off; * admits any origin; demo self-origin shim still works under --allow-origin; empty array (allowOrigins: []) regression anchor.
  • Capability registry pinning: allow_origin added to EXPECTED_REGISTERED_FEATURES and the CONDITIONAL_SERVE_FEATURES drift-insurance test.
  • npm test -w @qwen-code/qwen-code -- src/serve/auth.test.ts src/serve/server.test.ts → 289 / 289 ✓
  • npm run build → 0 errors

End-to-end smoke (manual, against a freshly built daemon):

node $WT/dist/cli.js serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173 &

# Capability advertised conditionally
curl -s http://127.0.0.1:4170/capabilities | jq '.features | map(select(. == "allow_origin"))'
# → ["allow_origin"]

# Matched origin → 200 + CORS headers
curl -isD- -H 'Origin: http://localhost:3000' http://127.0.0.1:4170/capabilities | head -10
# → 200, Access-Control-Allow-Origin: http://localhost:3000, Vary: Origin

# OPTIONS preflight → 204
curl -isD- -X OPTIONS -H 'Origin: http://localhost:3000' \
  -H 'Access-Control-Request-Method: POST' \
  http://127.0.0.1:4170/session/foo/prompt | head -10
# → 204, full CORS headers, no body

# Unmatched origin → 403 from existing wall
curl -isD- -H 'Origin: https://evil.example.com' http://127.0.0.1:4170/capabilities | head -5
# → 403 {"error":"Request denied by CORS policy"}, Vary: Origin

# Boot-refuse on `*` + no token
node $WT/dist/cli.js serve --allow-origin '*'
# → exit 1, "Refusing to start with --allow-origin '*' but no bearer token..."

Out of scope (follow-ups)

  • Access-Control-Allow-Credentials: true and the cookie-auth deployment model — needs a separate flag plus the CORS-spec "no * with credentials" boot check.
  • Wildcard subdomain patterns (https://*.example.com) — operators with N subdomains list N origins. Adds parser footguns (e.g. https://*.evil.example.com matching https://attacker.evil.example.com.attacker.com).
  • Per-route CORS policy — daemon has one allowlist for the whole surface; every route shares one auth model.

Docs

  • docs/developers/qwen-serve-protocol.md — new --allow-origin section in Authentication, plus allow_origin row in the conditional-tags table.
  • docs/users/qwen-serve.md — feature bullet next to the CORS-deny description, plus full --allow-origin row in the CLI flags table including the unsupported-subdomain-wildcard hint and the Origin: null always-rejected note.

🤖 Generated with Qwen Code

Replace the unconditional `denyBrowserOriginCors` 403-wall with a
configurable allowlist when `--allow-origin <pattern>` is set. Each
pattern is either `*` (any origin, refuses to boot without a bearer
token) or a canonical URL origin validated by round-tripping through
`new URL(...).origin`. Matched origins receive standard CORS response
headers (`Access-Control-Allow-Origin: <echoed>`, `Vary: Origin`,
methods/headers/max-age) plus 204 short-circuit for OPTIONS preflight;
unmatched origins keep today's 403 envelope. `Origin: null` is always
rejected even under `*`. Conditional capability tag `allow_origin`
advertised when the flag is set so SDK/webui clients can pre-flight.

When `--allow-origin` is unset the install path is unchanged and
today's behavior is preserved bit-for-bit. Loopback self-origin hits
are unaffected — the existing demo-page Origin-strip shim runs first.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in CORS allowlist to qwen serve via a new repeatable --allow-origin <pattern> flag, replacing the unconditional browser-Origin 403 “wall” when configured and advertising a new conditional capability tag (allow_origin) so browser clients can feature-detect support.

Changes:

  • Add --allow-origin CLI flag + boot-time validation (parseAllowOriginPatterns) with a * wildcard option and guardrails.
  • Install new allowOriginCors() middleware when allowlist patterns are configured; preserve existing denyBrowserOriginCors behavior when unset.
  • Advertise allow_origin conditionally in /capabilities, and add/extend tests and docs for the new behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/cli/src/serve/types.ts Adds allowOrigins to ServeOptions with detailed behavior/compat notes.
packages/cli/src/serve/auth.ts Implements allowlist parsing/error type and new allowOriginCors middleware.
packages/cli/src/serve/runQwenServe.ts Validates allowlist at boot and adds operator breadcrumb/warning.
packages/cli/src/serve/server.ts Switches CORS middleware install path based on allowOrigins; gates capability advertisement.
packages/cli/src/serve/capabilities.ts Registers allow_origin and adds conditional toggle plumbing.
packages/cli/src/commands/serve.ts Adds the --allow-origin CLI option and wires it into serve options.
packages/cli/src/serve/auth.test.ts Adds unit tests for parsing and CORS middleware behavior.
packages/cli/src/serve/server.test.ts Adds integration tests covering CORS allowlist install + capability tag behavior.
docs/users/qwen-serve.md Documents --allow-origin and default CORS posture.
docs/developers/qwen-serve-protocol.md Documents allowlist behavior and capability tag semantics in the protocol reference.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/commands/serve.ts
Comment thread docs/users/qwen-serve.md Outdated
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread packages/cli/src/serve/runQwenServe.ts
Comment thread docs/users/qwen-serve.md Outdated
@github-actions

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR implements a configurable CORS allowlist for the Qwen daemon, replacing the unconditional browser-origin rejection with an opt-in allowlist via --allow-origin <pattern>. The implementation is well-designed, thoroughly tested, and maintains backward compatibility. The code demonstrates strong security awareness with proper validation, strict parsing, and thoughtful edge-case handling.

🔍 General Feedback

Positive aspects:

  • Excellent security-first design with strict validation and clear threat modeling
  • Comprehensive test coverage (25+ tests for auth functions, 10+ for server integration)
  • Bit-for-bit backward compatibility when --allow-origin is unset
  • Clear documentation in code comments explaining the "why" behind decisions
  • Proper separation of concerns: validation at boot, middleware at runtime, capability advertisement
  • Thoughtful handling of edge cases (Origin: null, empty arrays, wildcard + no-token scenario)

Architecture decisions:

  • Single-middleware ownership of CORS policy is the right pattern
  • Boot-time validation prevents runtime surprises
  • Conditional capability tag (allow_origin) allows client pre-flighting without leaking the allowlist
  • Echoing origin verbatim (not literal *) paired with Vary: Origin follows CORS best practices

🎯 Specific Feedback

🟢 Medium

  1. File: packages/cli/src/serve/auth.ts:84 - The parseAllowOriginPatterns function lowercases origins for case-insensitive matching per RFC 6454 §4, which is correct. However, the comment mentions "scheme/host lowercase per RFC 6454 §4" but the code lowercases the entire origin including the port. While this works correctly (ports aren't case-sensitive), the comment could be more precise about what's being lowercased and why.

    Suggestion: Clarify comment to: "Lowercase the entire origin for case-insensitive comparison (scheme and host are case-insensitive per RFC 6454; port is preserved numerically but string-lowercase is safe for digits)."

  2. File: packages/cli/src/serve/auth.ts:147-157 - The Origin: null rejection includes a detailed comment explaining the security rationale (sandboxed iframes, file:// docs). This is excellent, but the comment speculates about future opt-in: "Operators who genuinely need null origins... can ask for an opt-in flag if/when that materializes."

    Suggestion: Consider removing the speculative future-flag language to avoid creating an expectation. The current strict behavior is well-justified on its own.

  3. File: packages/cli/src/serve/runQwenServe.ts:374-393 - The boot validation for --allow-origin '*' without a token is excellent. However, the error message is quite long (5 lines of string concatenation).

    Suggestion: Consider breaking this into a more readable template literal or multi-line string for maintainability, though the current approach is functional.

🔵 Low

  1. File: packages/cli/src/serve/auth.ts:56-62 - The InvalidAllowOriginPatternError constructor builds an error message with detailed guidance. The message is comprehensive but could benefit from including the canonical form when the pattern is a valid URL but has extra path/query components (the common typo case).

    Suggestion: When parsed.origin !== entry, include parsed.origin in the error message to show operators exactly what the correct value should be. Example:

    throw new InvalidAllowOriginPatternError(
      entry,
      `expected the canonical origin ${JSON.stringify(parsed.origin)} ` +
        'without trailing slash, path, userinfo, or query',
    );

    This is already done in the code—just noting it's a good pattern that could be emphasized more in the class-level JSDoc.

  2. File: packages/cli/src/serve/capabilities.ts:205 - The allow_origin capability is added to the registry with since: 'v1'. This is correct since it's part of the v1 protocol surface, but it's worth noting that clients checking for this feature will need to handle both its presence and absence gracefully (which the code already does via the conditional advertisement).

    Suggestion: Consider adding a comment in the JSDoc noting that clients should handle the absence of this tag as "CORS not configured" rather than "protocol version too old."

  3. File: packages/cli/src/commands/serve.ts:149-162 - The CLI flag description is comprehensive but quite long. The example is helpful but takes up significant space.

    Suggestion: Consider moving the detailed example to the user documentation and keeping the CLI help more concise. This is a minor style point—the current approach is functional and informative.

  4. File: packages/cli/src/serve/server.test.ts:5246-5260 - The test for "demo self-origin shim still works when --allow-origin is set" is an excellent regression anchor. The comment explains the rationale well. Consider adding a similar regression test for the case where opts.allowOrigins is explicitly set to an empty array ([]) to ensure it behaves identically to undefined.

    Note: Actually, I see this test exists at line 5320-5337 ("empty allowOrigins array behaves identically to undefined"). This is great—just noting the pattern is applied consistently.

  5. File: docs/users/qwen-serve.md:214 - The documentation table entry for --allow-origin is comprehensive. One minor suggestion: the phrase "Subdomain wildcards (https://*.example.com) are intentionally unsupported" could benefit from a brief explanation of why (e.g., "to prevent accidental over-permissioning; list each subdomain explicitly or use * with --require-auth").

    Note: The documentation does mention using * paired with --require-auth as an alternative, which is good. The rationale is implicit but could be more explicit.

✅ Highlights

  1. Security-first design: The boot-time validation that refuses --allow-origin '*' without a bearer token is an excellent safety mechanism. This prevents operators from accidentally exposing the daemon to any cross-origin request without authentication.

  2. Comprehensive test coverage: The test suite covers:

    • Pattern parsing (happy path, case insensitivity, wildcard, mixed patterns)
    • Every rejection branch (trailing slash, path, userinfo, non-URL, empty hostname, malformed entries)
    • CORS middleware behavior (matched/unmatched origins, OPTIONS preflight, Origin: null, wildcard)
    • Integration tests (capability advertisement, demo shim compatibility, empty array regression)
  3. Clear threat modeling: Comments throughout the code explain the security rationale, such as:

    • Why Origin: null is always rejected (sandboxed iframe attack vector)
    • Why the allowlist isn't exposed in /capabilities (enumeration risk)
    • Why echoing the origin verbatim (not *) is correct for browser caching
  4. Backward compatibility: The implementation preserves bit-for-bit behavior when --allow-origin is unset, ensuring existing deployments aren't affected.

  5. Documentation quality: Both inline comments and user-facing documentation are thorough, explaining not just what the feature does but why design decisions were made.

  6. Proper middleware ordering: The CORS middleware is installed before hostAllowlist and bearerAuth, ensuring it runs early in the request chain. The OPTIONS preflight short-circuit is safe because the actual request still runs through the full chain.

  7. Cache-aware headers: Setting Vary: Origin on both match and reject paths prevents intermediaries from serving stale responses to different origins—a subtle but important detail for production deployments behind proxies.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks — agreed, the docs/help/warning overstate the boot check. The actual gate is token configured (any source: --token / env / --require-auth), not --require-auth specifically. Will update the 5 affected spots (CLI help text, runQwenServe stderr breadcrumb, qwen-serve.md flag table + security section, qwen-serve-protocol.md --allow-origin section) to align with the implementation rather than tighten the code — the current gate already covers the real API surface, and forcing --require-auth would break legitimate * + token + loopback dev workflows that want to keep /health reachable for operator probes.

Copilot review on QwenLM#4527 caught a doc/code mismatch: 5 spots said `*` is
"only safe with --require-auth" but the actual boot check refuses `*`
only when no bearer token is configured (any source: --token, env, or
--require-auth). Update the wording in all 5 spots to match the
implementation, and call out the secondary loopback-only caveat that
/health and /demo remain pre-auth on loopback unless --require-auth is
set — operators with a `*` allowlist on loopback should pair with
--require-auth for full hardening.

Tightening the code instead would break legitimate `*` + token + loopback
dev workflows that want /health to remain reachable for k8s/Compose
probes; the actual API surface is gated regardless of --require-auth.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Comment thread packages/cli/src/serve/runQwenServe.ts
Comment thread packages/cli/src/serve/auth.ts
Comment thread packages/cli/src/serve/auth.ts
Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/cli/src/serve/auth.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented May 26, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report — PR 4527

Tested as the maintainer reviewer ahead of merge. Verified at head 6d61549cd (3 commits ahead of daemon_mode_b_main), against a fresh worktree on Linux, in a dedicated tmux session.

Build

Step Result
npm install OK (no new vulns introduced by this PR — pre-existing)
npm run build 0 errors, 15 pre-existing lint warnings, dist/cli.js produced (6.06 MB)

Unit tests

npm test -w @qwen-code/qwen-code -- src/serve/auth.test.ts src/serve/server.test.ts

294 / 294 passed in 13.77s (PR description said 289 — the extra 5 were added in 6d61549cd "fix(serve): address allow-origin review feedback", so the suite grew on top of the original count).

Live daemon smoke — flag set (--allow-origin http://localhost:3000 --allow-origin http://localhost:5173)

# Case Expected Result
T1 GET /capabilities features list allow_origin present
T2 Matched origin http://localhost:3000 200 + Access-Control-Allow-Origin: http://localhost:3000, Vary: Origin, Methods/Headers/Max-Age/Expose-Headers
T3 OPTIONS preflight (Access-Control-Request-Method: POST) 204 + full CORS headers, no body
T4 Unmatched https://evil.example.com 403 + Vary: Origin, no body leak
T5 No Origin header (CLI/SDK) 200 pass-through
T6 Origin: null (sandboxed-iframe attack vector) 403 with same envelope
T7 Case-insensitive match (HTTP://LocalHost:3000) 200 (RFC 6454 §4)
T8 Unmatched 403 body {"error":"Request denied by CORS policy"} — bit-for-bit match with denyBrowserOriginCors envelope
T9 Unmatched path 0 Access-Control-Allow-Origin headers (no leakage to evil origin)
T10 Matched origin reaches downstream /capabilities body fully delivered (v=1, 42 features incl. allow_origin)
T11 Plain OPTIONS (no preflight hdr) 200 (continues past 204 short-circuit)
T12 /demo self-origin shim under --allow-origin 200 (loopback strip runs before CORS)

Boot-time * + token gate

# Case Expected Result
T13a --allow-origin '*', no token exit 1, stderr names QWEN_SERVER_TOKEN / --token / specific origins ✅ exit 1, message identical to source
T13b --allow-origin '*' + QWEN_SERVER_TOKEN set boots with (WARNING: \*` admits any cross-origin browser …)` breadcrumb; any origin then admitted

Malformed-pattern boot rejection (all exit 1)

# Pattern Error message names canonical form?
T14a http://localhost:3000/ ✅ "expected the canonical origin http://localhost:3000"
T14b http://localhost:3000/app ✅ same
T14c localhost:3000 (no scheme) ✅ canonical resolves to null → rejected
T14d http://user:pass@localhost:3000 ✅ userinfo stripped in canonical guidance

serve --help

T15 — --allow-origin advertised with full description including the boot-refuse-on-* invariant and the caps.features.allow_origin pre-flight pointer. ✅

Backward-compat regression (flag unset)

# Case Expected Result
T16a caps.features.allow_origin absent
T16b Any browser Origin (matched or otherwise) 403 (today's denyBrowserOriginCors)
T16c No Origin header 200

The "no flag → bit-for-bit prior behavior" claim in the PR description holds end-to-end.

Observations for the merge decision

  • Single-middleware ownership of the CORS policy is what's actually shipped — denyBrowserOriginCors is replaced, not layered, when --allow-origin is set. Confirmed via packages/cli/src/serve/server.ts:526-539 (if (opts.allowOrigins?.length) … else { app.use(denyBrowserOriginCors); }).
  • Defense in depth on the *+no-token invariant: the gate runs both at boot in runQwenServe.ts:374-388 and again inside createApp (server.ts:528-535) for embedded callers that construct the app directly. T13a observed the boot one; embedded callers cannot bypass it.
  • No header leakage on reject (T9) is the property that makes the unmatched-403 a true CORS denial, not an over-eager allow + downstream rejection.
  • The Origin: null rejection (T6) holds even when * is configured — confirms the carve-out for sandboxed-iframe/file:// attack vectors.
  • Lint warnings on the build are all pre-existing (not introduced by this PR's files).

Verdict

Implementation behaves exactly as the PR description and follow-up review thread describe. Backward-compat regression passes. No surprises in the live daemon. Recommendation: ready to merge once the daemon_mode_b_main integration branch is otherwise green.

Verified locally on Linux 6.12.63 / Node from repo node_modules, worktree at qwen-code-pr4527, tmux session pr4527. Daemon bound to 127.0.0.1:4170, workspace /tmp.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 review at 6d61549cd: all 5 R1 Suggestions addressed cleanly (Vary: Origin on denyBrowserOriginCors, Access-Control-Expose-Headers, OPTIONS preflight gate, runQwenServe boot refusal test, createServeApp defensive * check). No new high-confidence findings in the 44-line incremental diff. Two low-confidence items for awareness only: (1) Vary: Origin missing on the no-Origin passthrough in allowOriginCors — shared-cache confusion risk behind corporate proxies; (2) no diagnostic logging on CORS rejection — operators have no server-side visibility into rejected origins. CI all_pass 21/21, 294 tests pass. — qwen3.7-max via Qwen Code /review

@doudouOUC doudouOUC requested review from chiga0 and yiliang114 May 26, 2026 12:02
@doudouOUC doudouOUC merged commit ead25b2 into QwenLM:daemon_mode_b_main May 26, 2026
23 checks passed
doudouOUC added a commit that referenced this pull request May 27, 2026
Squashed feature work from daemon_mode_b_main branch, rebased onto
latest main to establish proper merge-base and clean PR diff.

Original commits:
- perf(core): F2 cleanup PR A — R9/W11/W12/R10 (post-merge follow-ups) (#4411)
- refactor(acp-bridge): F1 test split — lift bridge.test.ts (6861 LOC) to acp-bridge (#4445)
- fix(core): F2 cleanup PR B — self-heal observability (W133-a + W134) (#4460)
- feat(sdk/daemon-ui): unified completeness follow-up to #4328 (#4353)
- docs(serve): v0.16-alpha known limits + SDK QWEN_SERVER_TOKEN env fallback (PR 27) (#4473)
- docs(deploy): local launch templates for v0.16-alpha (PR 30a) (#4483)
- feat(daemon+sdk): cross-client real-time sync completeness (#4484)
- feat(serve): add POST /session/:id/recap (#4504)
- feat(daemon): add voterClientId to permission_resolved (A4) (#4539)
- feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514) (#4527)
- feat(daemon): in-session model switch reaches the bus (A1) (#4546)
- feat(serve): prompt absolute deadline + SSE writer idle timeout (#4514 T2.9) (#4530)
- Feat/daemon react cli (#4380)
doudouOUC added a commit that referenced this pull request May 27, 2026
)

* feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514)

Replace the unconditional `denyBrowserOriginCors` 403-wall with a
configurable allowlist when `--allow-origin <pattern>` is set. Each
pattern is either `*` (any origin, refuses to boot without a bearer
token) or a canonical URL origin validated by round-tripping through
`new URL(...).origin`. Matched origins receive standard CORS response
headers (`Access-Control-Allow-Origin: <echoed>`, `Vary: Origin`,
methods/headers/max-age) plus 204 short-circuit for OPTIONS preflight;
unmatched origins keep today's 403 envelope. `Origin: null` is always
rejected even under `*`. Conditional capability tag `allow_origin`
advertised when the flag is set so SDK/webui clients can pre-flight.

When `--allow-origin` is unset the install path is unchanged and
today's behavior is preserved bit-for-bit. Loopback self-origin hits
are unaffected — the existing demo-page Origin-strip shim runs first.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* docs(serve): align --allow-origin '*' wording with the actual boot gate

Copilot review on #4527 caught a doc/code mismatch: 5 spots said `*` is
"only safe with --require-auth" but the actual boot check refuses `*`
only when no bearer token is configured (any source: --token, env, or
--require-auth). Update the wording in all 5 spots to match the
implementation, and call out the secondary loopback-only caveat that
/health and /demo remain pre-auth on loopback unless --require-auth is
set — operators with a `*` allowlist on loopback should pair with
--require-auth for full hardening.

Tightening the code instead would break legitimate `*` + token + loopback
dev workflows that want /health to remain reachable for k8s/Compose
probes; the actual API surface is gated regardless of --require-auth.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(serve): address allow-origin review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
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.

4 participants