1
0
Files
spring-security-demo/cors-csrf/docs/03-three-identical-403s.md
2026-08-28 10:16:21 +05:30

3.3 KiB

3. The three identical 403s

Prev: 2. Who resolves the CorsConfigurationSource · Next: 4. Preflight handlers

DefaultCorsProcessor runs three checks on a preflight, in order: origin, method, request headers. All three failures produce the same thing.

HTTP/1.1 403
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers

Invalid CORS request

Same status, same body, byte for byte, no Access-Control-* header to distinguish them. The assertion in CorsContractTests.threeRejectionsLookIdentical compares the three bodies for equality, so if a future version starts distinguishing them, that test fails.

The only place the difference exists is a DEBUG log line, and the three are worth memorising because they are the fastest CORS diagnosis available:

o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
o.s.web.cors.DefaultCorsProcessor : Reject: HTTP 'DELETE' is not allowed
o.s.web.cors.DefaultCorsProcessor : Reject: headers '[authorization]' are not allowed

Turn them on with:

logging.level.org.springframework.web.cors: DEBUG

The complete set of messages, read out of the class's constant pool, is five:

Message Meaning
Skip: no CORS configuration has been provided The source returned null for this path — chapter 2
Skip: response already contains "Access-Control-Allow-Origin" Something upstream already handled it
Reject: origin is malformed The Origin header did not parse
Reject: '…' origin is not allowed
Reject: HTTP '…' is not allowed
Reject: headers '[…]' are not allowed

Status codes, and what each one means

Collecting the states this module reproduces:

What you see What it means
401/403, no Access-Control-* at all No CorsFilter in the chain. The preflight was judged by authorization — chapter 1
200, no Access-Control-* CorsFilter is present and found no configuration for this path — chapter 2
403, Invalid CORS request CorsFilter is present and rejected origin, method or headers — this chapter
200 with Access-Control-Allow-Origin It worked
404, no Access-Control-* The path is outside the pattern you registered. Common with /api/** versus a mis-typed URL

The browser reports the same "blocked by CORS policy" for the first four rows. Two of them are not CORS problems.

What a simple request does

Only preflighted requests get intercepted. A simple GET runs the whole chain, so an unauthenticated one returns 401 — carrying the CORS header, because CorsFilter at 1000 already wrote it before AuthorizationFilter at 4200 rejected the request:

HTTP/1.1 401
Access-Control-Allow-Origin: https://spa.example.com
Access-Control-Allow-Credentials: true

That is the good case: the SPA's fetch resolves and the code can read response.status. It is also the reason "my POST fails but my GET returns a readable 401" is a coherent bug report and not a contradiction.


Prev: 2. Who resolves the CorsConfigurationSource · Next: 4. Preflight handlers