fix(19.3-06): credentialed CORS refuses wildcard-host and unanchored origins (WR-04)

- with supports_credentials, a * origin must be the whole leftmost host
  label, patterns must be anchored with ^ and $, and no origin rule may
  match the probe origins outside any configured domain
- globally and per profile; non-credentialed configs load as before
This commit is contained in:
Jakub Zych
2026-10-15 22:25:43 +02:00
parent 072353663e
commit e32940d90f
4 changed files with 131 additions and 4 deletions

View File

@@ -229,6 +229,12 @@ This fragment belongs in `config/http.yaml`. List the frontend's exact origin; `
The rules surf applies to the origin answer:
- `supports_credentials: true` together with `*` in `allowed_origins` fails the start-up. A credentialed answer that trusted any origin would let every site read responses made with the user's cookies; Laravel's php-cors reflects the origin in that case, surf refuses it.
- With `supports_credentials: true`, three more rules close other spellings of the same exposure, and each refusal names the key with its index, such as `http.cors.allowed_origins[1]`:
- an `allowed_origins` entry with a `*` is accepted only when the scheme is literal and the `*` is the whole leftmost host label, as in `https://*.example.com`; `https://*`, `*://*`, `https://*example.com` and `http*://www.example.com` fail the start-up;
- every `allowed_origins_patterns` entry must be anchored with `^` and `$`, as in `^https://[a-z]+\.example\.com$`; an unanchored pattern matches inside any origin, so `https://.*\.example\.com` would trust `https://x.example.com.attacker.net`;
- no origin rule may match an origin outside the configured domains: surf tries each rule against probe origins such as `https://cors-probe.invalid` and `null`, which refuses `https://*.com` and `^https?://.*$`.
Without credentials these entries load as before.
- An allowed origin is echoed exactly in `Access-Control-Allow-Origin`; `*` is sent only for `allowed_origins: ["*"]` without credentials, and that answer is unchanged from earlier releases.
- When the answer depends on the request's `Origin` (with `supports_credentials`, with any pattern, or with more than one listed origin), every response on a CORS path carries `Vary: Origin`, the preflight and a disallowed origin's response included, so a shared cache never serves one origin's answer to another.
- An `allowed_origins` entry with a `*` inside, such as `https://*.example.com`, matches like Laravel's `Str::is`: the `*` stands for any characters and the rest is literal.