Add the cors-csrf module
This commit is contained in:
79
cors-csrf/docs/05-the-error-dispatch.md
Normal file
79
cors-csrf/docs/05-the-error-dispatch.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 5. The `/error` dispatch, or why your 403 arrives as a 401
|
||||
|
||||
*Prev: [4. Preflight handlers](04-preflight-handlers.md) · Next: [6. CSRF for SPAs](06-csrf-for-spas.md)*
|
||||
|
||||
Two of the failures in this module — the wildcard/credentials clash in chapter 4 and the
|
||||
CSRF rejection in chapter 6 — produce a status code that has nothing to do with what went
|
||||
wrong. Both have the same cause, and it is worth understanding once because it explains a large
|
||||
fraction of confusing Spring Security bug reports.
|
||||
|
||||
## The mechanism
|
||||
|
||||
1. Something inside the chain calls `response.sendError(403, ..)` (that is what
|
||||
`AccessDeniedHandlerImpl` does) or lets an exception escape `FilterChainProxy`.
|
||||
2. The servlet container does not write that response. It **re-dispatches** the request
|
||||
internally to `/error`, with `DispatcherType.ERROR`.
|
||||
3. Spring Boot registers `springSecurityFilterChain` for **every** dispatcher type:
|
||||
`SecurityFilterProperties.dispatcherTypes` defaults to `EnumSet.allOf(DispatcherType.class)`.
|
||||
So the whole security chain runs again on that dispatch.
|
||||
4. On the second pass, the filters that extend `OncePerRequestFilter` skip themselves —
|
||||
`shouldNotFilterErrorDispatch()` defaults to `true`. `BasicAuthenticationFilter` is one of
|
||||
them. The credential is never re-read.
|
||||
5. The filters that extend `GenericFilterBean` do run. `AuthorizationFilter` is one of them.
|
||||
6. So the second pass is **authorized but not authenticated**: `AuthorizationFilter` evaluates
|
||||
`/error` against `anyRequest().authenticated()`, finds an anonymous principal, and denies it.
|
||||
7. The 401 from step 6 is what reaches the client. The 403 from step 1 is gone.
|
||||
|
||||
The mechanism is set out in full in
|
||||
[The Spring Security Filter Chain Explained](https://ankurm.com/spring-security-filter-chain-explained/);
|
||||
this chapter is what it looks like when it lands on a CORS or CSRF problem.
|
||||
|
||||
## Proving it in one diff
|
||||
|
||||
The `errorpermit` profile adds one filter chain, `@Order(0)`, matching `/error` and permitting
|
||||
everything. Nothing else changes.
|
||||
|
||||
```
|
||||
./scripts/run.sh csrfnaive POST → 401, empty body, WWW-Authenticate: Basic
|
||||
./scripts/run.sh csrfnaive,errorpermit POST → 403, {"status":403,"error":"Forbidden", ...}
|
||||
```
|
||||
|
||||
[`docs/output/08-csrf-naive.txt`](output/08-csrf-naive.txt) against
|
||||
[`docs/output/09-error-dispatch.txt`](output/09-error-dispatch.txt).
|
||||
|
||||
## What to do about it
|
||||
|
||||
Permit `/error`. It is not a hole: the error page is generated from an attribute the container
|
||||
set, and an unauthenticated request cannot reach it except through a dispatch the container
|
||||
initiated.
|
||||
|
||||
```java
|
||||
@Bean
|
||||
@Order(0)
|
||||
SecurityFilterChain errorChain(HttpSecurity http) throws Exception {
|
||||
return http.securityMatcher("/error")
|
||||
.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll())
|
||||
.csrf(CsrfConfigurer::disable)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
The alternative is to narrow the dispatcher types so the chain does not run on the error
|
||||
dispatch at all:
|
||||
|
||||
```yaml
|
||||
spring.security.filter.dispatcher-types: request
|
||||
```
|
||||
|
||||
That one is broader in effect than it looks; prefer the `/error` chain unless you have a
|
||||
specific reason.
|
||||
|
||||
## Why this matters more for a SPA than for a server-rendered app
|
||||
|
||||
A browser will not let a SPA read a cross-origin response unless the CORS headers are present.
|
||||
When the 403 is replaced by a 401 written on a dispatch where `CorsFilter` may or may not have
|
||||
re-run, what the developer sees in the console is neither "403" nor "CSRF"; it is
|
||||
`TypeError: Failed to fetch`. Every layer of the stack has thrown away the actual cause by then.
|
||||
|
||||
---
|
||||
*Prev: [4. Preflight handlers](04-preflight-handlers.md) · Next: [6. CSRF for SPAs](06-csrf-for-spas.md)*
|
||||
Reference in New Issue
Block a user