refactor(cors): reject cross-origin relay handshakes and drop wildcard def… - #1236
refactor(cors): reject cross-origin relay handshakes and drop wildcard def…#1236sudhir-intc wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1236 +/- ##
==========================================
+ Coverage 50.82% 51.02% +0.19%
==========================================
Files 149 149
Lines 13872 13930 +58
==========================================
+ Hits 7051 7108 +57
- Misses 6217 6218 +1
Partials 604 604 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Hardens Console’s CORS and WebSocket relay behavior to mitigate cross-site WebSocket hijacking and overly-permissive CORS defaults, while keeping same-origin behavior working for the embedded UI and non-browser clients.
Changes:
- Adds an
Originallowlist check to the KVM/SOL/IDER relay WebSocket upgrader (rejecting opaque origins and cross-origin by default). - Removes wildcard CORS defaults and introduces explicit default origins/headers with credentials supported for enumerated origins.
- Adds config validation and tests around CORS/Origin behavior and safer defaults.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/app/app.go | Adds relay Origin validation and adjusts CORS credential handling when * is present. |
| internal/app/app_test.go | Adds tests for the relay origin checker and CORS header behavior. |
| config/config.yml | Updates shipped default CORS origins/headers and documents why * is unsafe. |
| config/config.go | Updates in-memory defaults, adds validation for empty allowed_origins. |
| config/config_test.go | Updates default expectations and adds validation tests for allowed origins. |
| .env.example | Updates example env vars for explicit CORS origins/headers and credentials. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
internal/app/app.go:183
- parseOrigin currently accepts URLs with a non-empty path/query/fragment (e.g. "https://allowed.example/path"), and normalizeOrigin then silently drops the path when building the comparison key. Origins per RFC 6454 are strictly scheme://host[:port] (no userinfo/query/fragment, and no path beyond an optional trailing "/"), so this behavior can make a misconfigured allowlist more permissive than intended and can diverge from the CORS middleware’s exact Origin matching.
originURL, err := url.Parse(origin)
if err != nil || originURL.Scheme == "" || originURL.Host == "" {
return nil
}
8f2ff8a to
d51b54a
Compare
fix(cors): reject cross-origin relay handshakes and drop wildcard default
The KVM/SOL/IDER relay accepted websocket connections from any origin,
enabling Cross-Site WebSocket Hijacking, and allowed_origins defaulted to
"*", so the API answered every request with Access-Control-Allow-Origin: *.
- Validate the relay's Origin header against the configured allowlist.
- Do not honor "*" for the relay: a wildcard or empty allowlist degrades to
same-origin only, which closes the hijack on installs that still carry "*"
on disk. Same-origin is always accepted, so the embedded UI is unaffected.
Opaque origins ("null", data:, file:) are rejected.
- Compare hosts case-insensitively, matching the CORS middleware. A
mixed-case entry previously passed CORS but failed the relay.
- Replace the "*" allowed_headers with an explicit list, and reject an empty
allowed_origins at startup instead of panicking in the CORS library.
- Remove the wildcard from the shipped config.yml and .env.example, and warn
at startup if it is still configured.
Deployments serving the UI from a separate origin while relying on "*" must
now list that origin explicitly. Same-origin deployments are unaffected.
Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Problem :
The KVM/SOL/IDER relay accepted WebSocket connections from any origin, enabling Cross-Site WebSocket Hijacking: a malicious page could open a relay using a session the victim's browser attaches automatically. Separately, the allowed-origins setting defaulted to *, so the API answered every request with Access-Control-Allow-Origin: *, letting any website read API responses.
Changes
Compatibility.
Same-origin deployments are unaffected: the CORS middleware exempts same-origin before validating, so a Console reached at its own LAN address is not rejected by a localhost-only allowlist.
Here's some tests to check the functionality against main
Verification with curl
Expected outputs below are derived from reading the CORS library, the WebSocket library, and the relay handler — they have not been run live. Header order and letter case can shift a little between HTTP versions and Go releases; the lines themselves are the assertion.
Tests 1–3 need
auth.disabled: true, which skips the redirection-token check so curl can reach the origin check. Otherwise you need a token signed withauth.jwtKeywhosedeviceIdmatches?host=, sent inSec-Websocket-Protocol.Config L (what
mainships). The wildcard matters here: under it the CORS middleware passes everything, so the request reaches the WebSocket upgrader and the test isolates the origin check.Config F (this PR): same, but
allowed_origins: ["http://localhost:4200"], the explicitallowed_headerslist, andallow_credentials: true.Three things apply to every command below.
-k— withcertFile/keyFileempty Console generates a self-signed certificate at startup, which curl would otherwise refuse.--http1.1on the WebSocket tests (1–3) — the upgrade relies on theConnectionandUpgradeheaders, which HTTP/2 forbids, and Console negotiates h2 over TLS by default. Without the flag the handshake never reaches the origin check. Tests 4–6 run over h2, which is why curl prints their header names in lowercase.grepfilter so only the deciding lines show. On a successful upgrade curl prints101and then holds the connection until--max-time 3expires, exiting 28 — that timeout is expected, not a failure.1. Cross-Site WebSocket Hijacking (CM-323) — Config L
main— any website can open the relay:this PR — refused:
The body carries two lines because the WebSocket library writes
Forbiddenwith the 403, then the handler appends its own message after the status is already committed.2. Same-origin relay still works — Config L (regression guard)
Identical on both builds. This is the one that proves the hardening doesn't
break the built-in UI:
3.
Origin: null— Config LTest 1 with
-H 'Origin: null'. This is what sandboxed iframes anddata:/file:pages send, and the header in the pentest evidence.mainreturns the same101block as test 1; this PR returns the same403block.4. Wildcard CORS (#6801) — Config L vs F
main(Config L) — reproduces the finding. Noteaccess-control-allow-origin: *and the complete absence of a
vary:line:this PR (Config F) — rejected outright, no CORS headers at all:
The same split shows on a normal request:
main(Config L):this PR (Config F):
5. Allowed origin gets the complete header set — Config F
PATCHandIf-Matchare both present — the two the hand-rolled middlewarewould have dropped, breaking 8 routes and every optimistic-concurrency update.
6.
Vary: Origin— cache poisoningmain(Config L) — a shared cache can serve this to any other site:this PR (Config F) — origin echoed, and
varytells caches the response isorigin-dependent:
7. Mixed-case allowlist — correctness check
Not a vulnerability demo:
mainhas no relay origin check at all, so there isnothing to compare against. This only confirms the new allowlist is robust. With
allowed_origins: ["https://Console.Example:8181"], a request carryingOrigin: https://console.example:8181is accepted by both the API and the relayon this PR — previously such an entry passed CORS but failed the relay.