feat(cors): let a UI on another origin connect, static host previews included (#2362)

## What this does

Server changes so the UI can be served from another origin, such as a
Cloudflare Pages project and its previews, against an existing backend.
It ships before the frontend, which is parked on
[`backup/static-frontend-1a`](https://github.com/ppicazo/CoreScope/tree/backup/static-frontend-1a)
and will come as its own PR.

- **Exposed headers:** allowed origins get
`Access-Control-Expose-Headers: Retry-After, X-CoreScope-Load-Status`.
The UI reads both, and cross-origin they are `null` without this.
- **Wildcard origins:** an entry may be `scheme://*.host[:port]`, such
as `https://*.myproject.pages.dev`. It matches exactly one leftmost
label (each deploy and branch preview) for both CORS and `/ws`, but not
`myproject.pages.dev` itself. The part after `*.` needs at least three
labels, so `*.pages.dev` is refused. Refused or malformed wildcards are
dropped at startup with a log line. That floor is a sanity check, not a
Public Suffix List lookup: `https://*.s3.amazonaws.com` passes and would
admit every S3 website endpoint, so a wildcard belongs only over a host
the operator controls.
- **Case:** exact entries now match case-insensitively for CORS (was
`==`), as they already did for `/ws`.
- **Test only:** pins that the three anonymous POSTs (`/api/decode`,
`/api/packets/observations`, `/api/paths/inspect`) accept a `text/plain`
JSON body, which keeps them free of a preflight.

Credentials stay off, so nothing a listed origin sends rides on cookies.
Beyond GET, a listed origin, and so every deployment under a wildcard
entry, can:

- call those three anonymous POSTs as simple requests. That predates
this PR; they compute an answer and write nothing.
`Access-Control-Allow-Methods: GET, HEAD, OPTIONS` only governs
preflighted requests.
- with `userManagement.enabled`, use the existing bearer tier for
companion linking (`/api/auth/device-token`, `/api/auth/me`,
`/api/auth/logout`, `/api/account/settings`, `/api/account/companions*`)
with POST, PUT and DELETE. Those routes need a device token in
`Authorization`, not a cookie, but a preview deployment gets them too.
On such a server, list a wildcard only if previews may use them.

## Configuring

```yaml
- CORS_ALLOWED_ORIGINS=https://existing-embed.example,https://myproject.pages.dev,https://*.myproject.pages.dev
```

The env var replaces `corsAllowedOrigins`, so keep the origins already
listed there.

## Testing

- `go test ./...`, `go vet` and `gofmt` are clean in `cmd/server`. The
new tests were written first and failed first.
- In Chromium, the static build from the backup branch, served on
`pr-1.proj.preview.localhost`, passed its split E2E (9/9, WebSocket
included) against this server.
- From the same setup, the project origin connected, a two-level
subdomain was blocked, and `X-CoreScope-Load-Status` read `ready` where
master gives `null`.

## Performance

These run per request in the CORS middleware and the `/ws` handshake,
and they allocate nothing. Same-origin requests skip them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul PicazoandClaude Opus 5.5 authored and GitHub committed 2026-11-08 17:46:17 +01:00
1 parent 899a3ad0b1
commit d66a219bcb
7 files changed
+360 -14

No files matched your search

+6
View File
@@ -154,6 +154,10 @@ type Config struct {
// CORSAllowedOrigins is the list of origins permitted to make cross-origin
// requests. When empty (default), no Access-Control-* headers are sent,
// so browsers enforce same-origin policy. Set to ["*"] to allow all origins.
// An entry may use one wildcard label, "https://*.myproject.pages.dev"
// (see originMatches); LoadConfig drops malformed wildcards and those over
// fewer than three labels, a sanity floor rather than a Public Suffix List
// check (see wildcardOrigin).
CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"`
// WebSocket carries the /ws transport limits from #1794. Omitted entirely
@@ -530,12 +534,14 @@ func LoadConfig(baseDirs ...string) (*Config, error) {
cfg.migrateDeprecatedConfig()
cfg.applyListLimitsDefaults()
applyCORSEnv(cfg)
cfg.CORSAllowedOrigins = sanitizeCORSOrigins(cfg.CORSAllowedOrigins)
return cfg, nil
}
cfg.NormalizeTimestampConfig()
cfg.migrateDeprecatedConfig()
cfg.applyListLimitsDefaults()
applyCORSEnv(cfg)
cfg.CORSAllowedOrigins = sanitizeCORSOrigins(cfg.CORSAllowedOrigins)
return cfg, nil // defaults
}
+90 -8
View File
@@ -1,6 +1,7 @@
package main
import (
"log"
"net/http"
"os"
"strings"
@@ -32,18 +33,98 @@ func applyCORSEnv(cfg *Config) {
cfg.CORSAllowedOrigins = out
}
// wildcardOrigin splits a wildcard allowlist entry, "scheme://*.host[:port]",
// into its scheme and the host[:port] the wildcard label sits on. One entry
// such as "https://*.myproject.pages.dev" then covers every deployment and
// branch preview of a static host project (85868e50.myproject.pages.dev).
// ok is false for anything else, including a wildcard over fewer than three
// labels: "https://*.pages.dev" would admit every Pages site on the internet.
// The label count is a sanity floor, not a Public Suffix List check. It keeps
// two-label shared suffixes (pages.dev, github.io, co.uk) out, but
// "https://*.s3.amazonaws.com" passes and would admit every S3 website
// endpoint, so a wildcard belongs only over a host the operator controls.
func wildcardOrigin(entry string) (scheme, rest string, ok bool) {
scheme, rest, ok = strings.Cut(entry, "://*.")
if !ok {
return "", "", false
}
if s := strings.ToLower(scheme); s != "http" && s != "https" {
return "", "", false
}
host, port, hasPort := strings.Cut(rest, ":")
if hasPort && (port == "" || strings.Trim(port, "0123456789") != "") {
return "", "", false
}
if strings.Count(host, ".") < 2 || strings.Contains(host, "..") ||
strings.HasPrefix(host, ".") || strings.HasSuffix(host, ".") ||
strings.ContainsAny(host, "*/?#@ ") {
return "", "", false
}
return scheme, rest, true
}
// originMatches reports whether a browser Origin matches one allowlist entry:
// the same origin ignoring case, or a wildcard entry (see wildcardOrigin)
// with exactly one more label in front. The wildcard does not match the host
// it sits on, so "https://myproject.pages.dev" is listed on its own. "*"
// matches nothing here: the CORS middleware handles it and /ws refuses it.
func originMatches(entry, origin string) bool {
if strings.EqualFold(entry, origin) {
return true
}
scheme, rest, ok := wildcardOrigin(entry)
if !ok {
return false
}
oScheme, oRest, ok := strings.Cut(origin, "://")
if !ok || !strings.EqualFold(scheme, oScheme) {
return false
}
label, tail, ok := strings.Cut(oRest, ".")
return ok && label != "" && !strings.ContainsAny(label, ":/@") && strings.EqualFold(tail, rest)
}
// sanitizeCORSOrigins drops each entry that has a wildcard but is neither "*"
// nor a valid wildcard label (see wildcardOrigin), and logs it, so a typo
// fails closed instead of matching more than the operator meant.
func sanitizeCORSOrigins(origins []string) []string {
var out []string
for _, o := range origins {
if o != "*" && strings.Contains(o, "*") {
if _, _, ok := wildcardOrigin(o); !ok {
log.Printf(`[config] corsAllowedOrigins: ignoring %q: a wildcard must be the whole leftmost label of "scheme://*.host", with at least three labels after "*.", e.g. "https://*.myproject.pages.dev"`, o)
continue
}
}
out = append(out, o)
}
return out
}
// corsExposedHeaders are the response headers the UI reads besides the
// CORS-safelisted ones: Retry-After (api() on a warm-up 503, node-reach.js on
// a 429) and X-CoreScope-Load-Status (warmup-banner.js). A UI served from an
// allowed origin reads both as null unless they are listed here.
const corsExposedHeaders = "Retry-After, X-CoreScope-Load-Status"
// corsMiddleware returns a middleware that sets CORS headers based on the
// configured allowed origins. When CORSAllowedOrigins is empty (default),
// no Access-Control-* headers are added, preserving browser same-origin policy.
// An entry is an exact origin, a wildcard label (originMatches) or "*".
//
// Embed contract (issue #1369): the cross-domain surface is read-only. The
// middleware advertises only GET, HEAD, and OPTIONS in Access-Control-Allow-
// Methods so iframes / server-side fetchers cannot opt into POST/PUT/DELETE
// via CORS. Same-origin writes (admin UI, API-key holders on the canonical
// origin) are unaffected — they never go through the preflight path.
// Embed contract (issue #1369): no cross-origin writes. The middleware
// advertises only GET, HEAD, and OPTIONS in Access-Control-Allow-Methods so
// iframes cannot opt into POST/PUT/DELETE via CORS. That header only governs
// requests with a preflight: a CORS simple request (a POST with a text/plain
// body) has none, which is how a listed origin calls the three anonymous POSTs
// that compute an answer and write nothing (/api/decode,
// /api/packets/observations, /api/paths/inspect; see simple_post_test.go).
// Same-origin writes (admin UI, API-key holders on the canonical origin) are
// unaffected — they never go through the preflight path.
// The exception is the bearer routes of companion linking (bearerCORSPath,
// only with user management on), which also allow POST, PUT, DELETE and the
// Authorization header.
// Authorization header, for every listed origin: each deployment under a
// wildcard entry included.
// Credentialed CORS is intentionally NOT enabled.
func (s *Server) corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -68,7 +149,7 @@ func (s *Server) corsMiddleware(next http.Handler) http.Handler {
wildcard = true
break
}
if o == reqOrigin {
if originMatches(o, reqOrigin) {
allowed = true
break
}
@@ -92,6 +173,7 @@ func (s *Server) corsMiddleware(next http.Handler) http.Handler {
w.Header().Set("Access-Control-Allow-Origin", reqOrigin)
w.Header().Set("Vary", "Origin")
}
w.Header().Set("Access-Control-Expose-Headers", corsExposedHeaders)
if s.auth != nil && bearerCORSPath(r.URL.Path) {
// Companion linking: CoreDrive RX on an allowlisted origin logs in
// and writes with a device token in Authorization. Still no
@@ -99,7 +181,7 @@ func (s *Server) corsMiddleware(next http.Handler) http.Handler {
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
} else {
// Read-only embed contract — see comment above.
// Embed contract — see comment above.
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"net/http/httptest"
"strings"
"testing"
)
// A frontend served from another origin (a static host listed in
// CORS_ALLOWED_ORIGINS) reads two response headers besides Content-Type:
// Retry-After (api() on a warm-up 503, node-reach.js on a 429) and
// X-CoreScope-Load-Status (warmup-banner.js). Cross-origin, fetch exposes only
// the CORS-safelisted headers unless the server lists the rest in
// Access-Control-Expose-Headers; without it both read as null.
func exposedHeaders(t *testing.T, origins []string, origin string) map[string]bool {
t.Helper()
handler := newTestServerWithCORS(origins).corsMiddleware(dummyHandler)
req := httptest.NewRequest("GET", "/api/stats", nil)
if origin != "" {
req.Header.Set("Origin", origin)
}
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
got := map[string]bool{}
for _, v := range rr.Header().Values("Access-Control-Expose-Headers") {
for _, name := range strings.Split(v, ",") {
if name = strings.TrimSpace(name); name != "" {
got[strings.ToLower(name)] = true
}
}
}
return got
}
func TestCORS_ExposesRetryAfterAndLoadStatus(t *testing.T) {
for _, tc := range []struct {
name string
origins []string
}{
{"listed origin", []string{"https://beta.example"}},
{"wildcard", []string{"*"}},
} {
got := exposedHeaders(t, tc.origins, "https://beta.example")
for _, want := range []string{"retry-after", "x-corescope-load-status"} {
if !got[want] {
t.Errorf("%s: Access-Control-Expose-Headers lacks %s, got %v", tc.name, want, got)
}
}
}
}
func TestCORS_NoExposeHeadersWithoutAnAllowedOrigin(t *testing.T) {
for _, tc := range []struct {
name string
origins []string
origin string
}{
{"no CORS list", nil, "https://beta.example"},
{"unlisted origin", []string{"https://beta.example"}, "https://evil.example"},
{"same-origin request (no Origin header)", []string{"https://beta.example"}, ""},
} {
if got := exposedHeaders(t, tc.origins, tc.origin); len(got) != 0 {
t.Errorf("%s: expected no Access-Control-Expose-Headers, got %v", tc.name, got)
}
}
}
+131
View File
@@ -0,0 +1,131 @@
package main
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
// A static host such as Cloudflare Pages serves each deployment and branch
// preview on its own subdomain of the project (85868e50.myproject.pages.dev,
// pr-12.myproject.pages.dev). One allowlist entry, "https://*.myproject.pages.dev",
// covers them for both CORS and /ws. The wildcard is one leftmost label and
// matches exactly one label; the project's own origin is listed separately.
func TestOriginMatches(t *testing.T) {
for _, tc := range []struct {
pattern, origin string
want bool
}{
{"https://beta.example", "https://beta.example", true},
{"https://Beta.Example", "https://beta.example", true},
{"https://beta.example", "https://beta.example:8443", false},
{"https://*.myproject.pages.dev", "https://85868e50.myproject.pages.dev", true},
{"https://*.myproject.pages.dev", "https://pr-12.myproject.pages.dev", true},
{"https://*.myproject.pages.dev", "https://PR-12.MyProject.pages.dev", true},
{"https://*.myproject.pages.dev", "https://myproject.pages.dev", false},
{"https://*.myproject.pages.dev", "https://a.b.myproject.pages.dev", false},
{"https://*.myproject.pages.dev", "https://evilmyproject.pages.dev", false},
{"https://*.myproject.pages.dev", "https://x.myproject.pages.dev.evil.example", false},
{"https://*.myproject.pages.dev", "http://x.myproject.pages.dev", false},
{"https://*.myproject.pages.dev", "https://x.myproject.pages.dev:8443", false},
{"https://*.myproject.pages.dev", "https://.myproject.pages.dev", false},
{"https://*.myproject.pages.dev", "null", false},
{"http://*.pr.preview.localhost:8788", "http://pr-1.pr.preview.localhost:8788", true},
{"http://*.pr.preview.localhost:8788", "http://pr-1.pr.preview.localhost", false},
// Patterns refused at config load never match either.
{"https://*.pages.dev", "https://anyone.pages.dev", false},
{"https://*.example.com", "https://www.example.com", false},
{"*", "https://beta.example", false},
} {
if got := originMatches(tc.pattern, tc.origin); got != tc.want {
t.Errorf("originMatches(%q, %q) = %v, want %v", tc.pattern, tc.origin, got, tc.want)
}
}
}
func TestSanitizeCORSOriginsDropsUnsafeWildcards(t *testing.T) {
in := []string{
"https://beta.example",
"*",
"https://myproject.pages.dev",
"https://*.myproject.pages.dev",
"http://*.pr.preview.localhost:8788",
"https://*.pages.dev", // every Pages site on the internet
"https://*.example.com", // fewer than three labels after "*."
"*.myproject.pages.dev", // no scheme
"https://a.*.pages.dev", // not the leftmost label
"https://*abc.myproject.pages.dev", // part of a label
"https://*.*.myproject.pages.dev",
"https://*.myproject.pages.dev/path",
"https://*.myproject.pages.dev:port",
"ftp://*.a.b.example",
"https://*.a..example",
}
want := []string{
"https://beta.example",
"*",
"https://myproject.pages.dev",
"https://*.myproject.pages.dev",
"http://*.pr.preview.localhost:8788",
}
if got := sanitizeCORSOrigins(in); !reflect.DeepEqual(got, want) {
t.Fatalf("sanitizeCORSOrigins:\n got %q\nwant %q", got, want)
}
}
func TestLoadConfigDropsUnsafeWildcardFromEnv(t *testing.T) {
t.Setenv("CORS_ALLOWED_ORIGINS", "https://myproject.pages.dev,https://*.myproject.pages.dev,https://*.pages.dev")
cfg, err := LoadConfig("/nonexistent")
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
want := []string{"https://myproject.pages.dev", "https://*.myproject.pages.dev"}
if !reflect.DeepEqual(cfg.CORSAllowedOrigins, want) {
t.Fatalf("CORSAllowedOrigins = %q, want %q", cfg.CORSAllowedOrigins, want)
}
}
func TestCORS_WildcardEntryEchoesTheMatchingOrigin(t *testing.T) {
handler := newTestServerWithCORS([]string{"https://*.myproject.pages.dev"}).corsMiddleware(dummyHandler)
for _, tc := range []struct {
origin string
allow bool
}{
{"https://85868e50.myproject.pages.dev", true},
{"https://myproject.pages.dev", false},
{"https://a.b.myproject.pages.dev", false},
} {
req := httptest.NewRequest("GET", "/api/stats", nil)
req.Header.Set("Origin", tc.origin)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
acao := rr.Header().Get("Access-Control-Allow-Origin")
if tc.allow {
if acao != tc.origin || rr.Header().Get("Vary") != "Origin" {
t.Errorf("%s: ACAO %q, Vary %q; want the origin echoed with Vary: Origin", tc.origin, acao, rr.Header().Get("Vary"))
}
} else if acao != "" {
t.Errorf("%s: expected no ACAO, got %q", tc.origin, acao)
}
}
}
func TestCheckOriginHonorsWildcardEntry(t *testing.T) {
srv, _ := newCheckOriginServer(t, []string{"https://myproject.pages.dev", "https://*.myproject.pages.dev"})
defer srv.Close()
for _, tc := range []struct {
origin string
want int
}{
{"https://pr-12.myproject.pages.dev", http.StatusSwitchingProtocols},
{"https://myproject.pages.dev", http.StatusSwitchingProtocols},
{"https://a.b.myproject.pages.dev", http.StatusForbidden},
{"https://evil.example", http.StatusForbidden},
} {
if status, _ := dialWS(t, srv, tc.origin); status != tc.want {
t.Errorf("%s: status %d, want %d", tc.origin, status, tc.want)
}
}
}
+58
View File
@@ -0,0 +1,58 @@
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)
// A UI served from another origin (a static host listed in
// CORS_ALLOWED_ORIGINS) sends its three anonymous JSON POSTs with Content-Type
// text/plain. That keeps each one a CORS simple request: no preflight, which
// the embed CORS tier (Allow-Methods GET, HEAD, OPTIONS) would
// refuse. It works because these handlers decode the body as JSON whatever its
// Content-Type. If a handler starts insisting on application/json, this fails
// here instead of on the static host.
func TestAnonymousPostsAcceptTextPlainJSON(t *testing.T) {
_, router := setupTestServerWithAPIKey(t, "")
// The full test server is still warming the inspect graph (503), and the
// bare server path_inspect_test.go uses has no node data to search. A
// prefix the handler rejects after decoding still proves the point: a body
// it could not decode would answer "invalid JSON" instead.
inspect := newTestServerForInspect(t)
cases := []struct {
path, body string
handler http.Handler
want int
wantBody string
}{
{"/api/decode", `{"hex":"0200"}`, router, http.StatusOK, `"decoded"`},
{"/api/packets/observations", `{"hashes":["abc123"]}`, router, http.StatusOK, `"results"`},
{"/api/paths/inspect", `{"prefixes":["abc"]}`, http.HandlerFunc(inspect.handlePathInspect), http.StatusBadRequest, "even-length hex"},
}
for _, c := range cases {
t.Run(c.path, func(t *testing.T) {
post := func(contentType string) *httptest.ResponseRecorder {
req := httptest.NewRequest("POST", c.path, bytes.NewBufferString(c.body))
req.Header.Set("Content-Type", contentType)
req.Header.Set("Origin", "https://beta.corescope.example")
w := httptest.NewRecorder()
c.handler.ServeHTTP(w, req)
return w
}
asJSON := post("application/json")
asText := post("text/plain;charset=UTF-8")
if asJSON.Code != c.want || !bytes.Contains(asJSON.Body.Bytes(), []byte(c.wantBody)) {
t.Fatalf("application/json: expected %d with %s, got %d (body: %s)", c.want, c.wantBody, asJSON.Code, asJSON.Body.String())
}
if asText.Code != asJSON.Code {
t.Fatalf("text/plain: got %d, application/json got %d (body: %s)", asText.Code, asJSON.Code, asText.Body.String())
}
if asText.Body.String() != asJSON.Body.String() {
t.Fatalf("text/plain answered differently:\n text/plain: %s\n json: %s", asText.Body.String(), asJSON.Body.String())
}
})
}
}
+7 -5
View File
@@ -17,13 +17,14 @@ type Hub struct {
mu sync.RWMutex
clients map[*Client]bool
upgrader websocket.Upgrader
allowedOrigins []string // exact-match allowlist for /ws CheckOrigin (see SetAllowedOrigins)
allowedOrigins []string // allowlist for /ws CheckOrigin (see SetAllowedOrigins)
limits *wsLimiter // #1794: per-IP caps and deny list; nil allows everything
pingInterval time.Duration
}
// SetAllowedOrigins configures the exact-match origin allowlist consulted by
// the WebSocket upgrader's CheckOrigin. The "*" wildcard is deliberately NOT
// SetAllowedOrigins configures the origin allowlist consulted by the
// WebSocket upgrader's CheckOrigin: exact origins, or one wildcard label
// scoped to a project (originMatches). The "*" wildcard is deliberately NOT
// honored here (it IS honored by the HTTP CORS middleware): OWASP's
// WebSocket Security Cheat Sheet recommends an explicit allowlist for CSWSH
// defense. If "*" appears in the slice, it is ignored and a startup WARN is
@@ -46,7 +47,8 @@ func (h *Hub) SetAllowedOrigins(origins []string) {
// - empty Origin header → allow (non-browser client; rate-limit / IP gate
// is handled separately, see #1794).
// - Origin host == request Host (same-origin) → allow.
// - Origin in allowedOrigins by exact case-insensitive match → allow.
// - Origin matches an allowedOrigins entry → allow (originMatches: exact
// and case-insensitive, or one wildcard label).
// - "*" in allowedOrigins is ignored (see SetAllowedOrigins).
// - anything else → reject (gorilla returns 403).
func (h *Hub) checkOrigin(r *http.Request) bool {
@@ -68,7 +70,7 @@ func (h *Hub) checkOrigin(r *http.Request) bool {
if o == "*" {
continue // deliberately not honored — see SetAllowedOrigins
}
if strings.EqualFold(o, origin) {
if originMatches(o, origin) {
return true
}
}
+1 -1
View File
@@ -40,7 +40,7 @@
},
"_comment_ingestorStats": "Ingestor publishes a 1-Hz stats snapshot consumed by the server's /api/perf/io and /api/perf/write-sources endpoints (#1120). Path is configured via the CORESCOPE_INGESTOR_STATS environment variable on the INGESTOR process. Default: /tmp/corescope-ingestor-stats.json. The writer uses O_NOFOLLOW + 0o600, so a pre-planted symlink in /tmp cannot be used to clobber an arbitrary file. SECURITY: in shared-tmp environments (multi-tenant hosts), point CORESCOPE_INGESTOR_STATS at a private directory like /var/lib/corescope/ingestor-stats.json that only the corescope user can write to.",
"corsAllowedOrigins": [],
"_comment_corsAllowedOrigins": "Cross-origin allowlist for embed scenarios (#1369) AND for /ws WebSocket upgrades (#1793 — CSWSH defense per OWASP WebSocket Security Cheat Sheet). Exact-match origins, e.g. [\"https://blog.example.com\", \"https://embed.example.com\"]. Same-origin requests (Origin host == request Host) and non-browser clients (no Origin header) are always allowed for /ws. When empty (default), no Access-Control-* headers are sent and browsers enforce same-origin; cross-origin /ws upgrades are rejected. When non-empty, only the listed origins receive CORS headers, and Access-Control-Allow-Methods is limited to GET, HEAD, OPTIONS (the cross-domain surface is read-only — same-origin admin writes are unaffected). One exception, with userManagement.enabled: the CoreDrive RX device-token routes (/api/auth/device-token, /api/auth/me, /api/auth/logout, /api/account/settings, /api/account/companions*) also allow POST, PUT, DELETE and the Authorization and Content-Type headers for listed origins, so an RX build on another origin can log in and link companions with a bearer token. Use [\"*\"] to allow any origin for CORS XHR (NOT recommended for write-capable deployments); note that \"*\" is deliberately NOT honored for /ws upgrades — list explicit origins to permit cross-origin WebSocket clients (OWASP guidance: avoid wildcards in CSWSH defense). Operators can override per-deployment with the CORS_ALLOWED_ORIGINS environment variable (comma-separated). No credentialed CORS is enabled. To embed the map or channels pages cross-domain, add the embedding origin here and use the URL pattern '/#/map?embed=1' or '/#/channels?embed=1' — embed mode hides the top-nav, bottom-nav, and side drawer for full-bleed iframe rendering.",
"_comment_corsAllowedOrigins": "Cross-origin allowlist for embed scenarios (#1369) AND for /ws WebSocket upgrades (#1793 — CSWSH defense per OWASP WebSocket Security Cheat Sheet). Exact-match origins, e.g. [\"https://blog.example.com\", \"https://embed.example.com\"]. An entry may also use one wildcard label for a static host project, e.g. \"https://*.myproject.pages.dev\": it matches exactly one more label (every Cloudflare Pages deployment and branch preview, such as https://85868e50.myproject.pages.dev) for both CORS and /ws, but not https://myproject.pages.dev itself, so list that too. The part after \"*.\" needs at least three labels: \"https://*.pages.dev\" (every Pages site on the internet) and \"https://*.example.com\" are ignored with a startup log line, as is any other wildcard that is not the whole leftmost label. That floor is a sanity check, not a Public Suffix List lookup: \"https://*.s3.amazonaws.com\" passes and would admit every S3 website endpoint, so put a wildcard only over a host you control. Same-origin requests (Origin host == request Host) and non-browser clients (no Origin header) are always allowed for /ws. When empty (default), no Access-Control-* headers are sent and browsers enforce same-origin; cross-origin /ws upgrades are rejected. When non-empty, only the listed origins receive CORS headers, and Access-Control-Allow-Methods is limited to GET, HEAD, OPTIONS, so they cannot write cross-origin (same-origin admin writes are unaffected). That limit only applies to requests with a preflight: listed origins can also call the three anonymous POSTs /api/decode, /api/packets/observations and /api/paths/inspect with a text/plain body, which compute an answer and write nothing. One exception, with userManagement.enabled: the CoreDrive RX device-token routes (/api/auth/device-token, /api/auth/me, /api/auth/logout, /api/account/settings, /api/account/companions*) also allow POST, PUT, DELETE and the Authorization and Content-Type headers for every listed origin, each deployment under a wildcard entry included, so an RX build on another origin can log in and link companions with a bearer token. Use [\"*\"] to allow any origin for CORS XHR (NOT recommended for write-capable deployments); note that \"*\" is deliberately NOT honored for /ws upgrades — list explicit origins to permit cross-origin WebSocket clients (OWASP guidance: avoid wildcards in CSWSH defense). Operators can override per-deployment with the CORS_ALLOWED_ORIGINS environment variable (comma-separated). No credentialed CORS is enabled. Listed origins can also read the Retry-After and X-CoreScope-Load-Status response headers (Access-Control-Expose-Headers), so the UI served from a static host on a listed origin handles warm-up and busy answers as the embedded UI does. To embed the map or channels pages cross-domain, add the embedding origin here and use the URL pattern '/#/map?embed=1' or '/#/channels?embed=1' — embed mode hides the top-nav, bottom-nav, and side drawer for full-bleed iframe rendering.",
"webSocket": {
"maxConnsPerIP": 0,
"_comment_maxConnsPerIP": "#1794 - concurrent /ws connections allowed from one client address. 0 = off, and that is the shipped default ON PURPOSE: carrier-grade NAT puts thousands of unrelated mobile subscribers behind one public IPv4, so a low cap refuses real visitors while a scraper simply rents more addresses. Set it only if you know your audience is not behind shared NAT.",