fix(GRO-2586): enforce trusted-origins allowlist on Better Auth CORS responses
CI / Test (pull_request) Successful in 28s
CI / Lint & Typecheck (pull_request) Successful in 33s
CI / Build & Push Docker Images (pull_request) Successful in 1m29s

Better Auth reflects the request Origin into Access-Control-Allow-Origin
unconditionally, bypassing the trustedOrigins config. An attacker-origin
page could XHR /api/auth/sign-in/social with credentials and read the OIDC
authorize URL + state from the response body.

- Add src/lib/auth-cors.ts: enforceAuthCors() wraps any Better Auth Response,
  stripping ACAO/ACAC for untrusted origins and enforcing the allowlist for
  trusted ones
- Wire enforceAuthCors() into the /api/auth/* handler in src/index.ts
- Add src/__tests__/authCors.test.ts: 6 regression tests covering trusted,
  untrusted, undefined, and empty-string origins
- Update UAT_PLAYBOOK.md §4.1 with TC-API-1.29/1.30/1.31 CORS test cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-06-26 13:29:56 +00:00
parent c01e4acf0a
commit 20a0c7eb92
4 changed files with 89 additions and 2 deletions
+3
View File
@@ -110,6 +110,9 @@ Expected: one row, `role = 'groomer'`. If zero rows return, the request hit the
| TC-API-1.26 | Auto-provision skipped during OOBE | During fresh setup (needsSetup: true), complete OIDC login — verify no duplicate staff record created before setup completes | No duplicate staff, OOBE completes successfully | Duplicate staff record, 403 before setup, auto-provision interferes with OOBE |
| TC-API-1.27 | Multi-origin CORS — demo host sign-in | `POST /api/auth/sign-in/social` with `callbackURL=https://demo.groombook.dev` | 200 OK, no origin-mismatch error | 400/403 "Origin mismatch" |
| TC-API-1.28 | Multi-origin CORS — farh.net host sign-in | `POST /api/auth/sign-in/social` with `callbackURL=https://groombook.farh.net` | 200 OK, no origin-mismatch error | 400/403 "Origin mismatch" |
| TC-API-1.29 | CORS — untrusted origin blocked (GRO-2586) | POST /api/auth/sign-in/social with `Origin: https://evil.example.com` header | Response has **no** `Access-Control-Allow-Origin` header — attacker origin is not reflected | `Access-Control-Allow-Origin: https://evil.example.com` present in response |
| TC-API-1.30 | CORS — trusted origin allowed (GRO-2586) | POST /api/auth/sign-in/social with `Origin: https://uat.groombook.dev` header | `Access-Control-Allow-Origin: https://uat.groombook.dev` + `Access-Control-Allow-Credentials: true` | CORS header absent or trusted origin rejected |
| TC-API-1.31 | CORS — untrusted preflight blocked (GRO-2586) | `curl -i -X OPTIONS https://uat.groombook.dev/api/auth/sign-in/social -H 'Origin: https://evil.example.com' -H 'Access-Control-Request-Method: POST'` | Response has **no** `Access-Control-Allow-Origin: https://evil.example.com` | Preflight reflects attacker origin |
### 4.2 Client Management