Add the cors-csrf module
This commit is contained in:
58
cors-csrf/docs/output/01-mvc-only.txt
Normal file
58
cors-csrf/docs/output/01-mvc-only.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
==============================================================================
|
||||
docs/output/01-mvc-only.txt
|
||||
CORS configured with WebMvcConfigurer.addCorsMappings and nothing else.
|
||||
Profile: mvconly
|
||||
==============================================================================
|
||||
|
||||
# The security chain. Note what is NOT in it.
|
||||
$ curl -s localhost:8080/diag/chain
|
||||
{
|
||||
"profiles": [
|
||||
"mvconly"
|
||||
],
|
||||
"chains": [
|
||||
{
|
||||
"size": 11,
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"LogoutFilter",
|
||||
"BasicAuthenticationFilter",
|
||||
"RequestCacheAwareFilter",
|
||||
"SecurityContextHolderAwareRequestFilter",
|
||||
"AnonymousAuthenticationFilter",
|
||||
"ExceptionTranslationFilter",
|
||||
"AuthorizationFilter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# CorsConfigurationSource beans in the context.
|
||||
$ curl -s localhost:8080/diag/cors-sources
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": false
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 401
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# The MVC CORS mapping is real - it just never runs, because the request is
|
||||
# rejected at AuthorizationFilter (order 4200) and the DispatcherServlet is
|
||||
# downstream of the entire filter chain.
|
||||
56
cors-csrf/docs/output/02-mvc-bridge.txt
Normal file
56
cors-csrf/docs/output/02-mvc-bridge.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
==============================================================================
|
||||
docs/output/02-mvc-bridge.txt
|
||||
The identical MVC CORS mapping plus one line: .cors(Customizer.withDefaults()).
|
||||
Profile: mvcbridge
|
||||
==============================================================================
|
||||
|
||||
$ curl -s localhost:8080/diag/chain
|
||||
{
|
||||
"profiles": [
|
||||
"mvcbridge"
|
||||
],
|
||||
"chains": [
|
||||
{
|
||||
"size": 12,
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"CorsFilter",
|
||||
"LogoutFilter",
|
||||
"BasicAuthenticationFilter",
|
||||
"RequestCacheAwareFilter",
|
||||
"SecurityContextHolderAwareRequestFilter",
|
||||
"AnonymousAuthenticationFilter",
|
||||
"ExceptionTranslationFilter",
|
||||
"AuthorizationFilter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Methods: GET,POST
|
||||
Access-Control-Allow-Headers: content-type, x-xsrf-token
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Max-Age: 1800
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# CorsFilter is now in the chain at order 1000, between HeaderWriterFilter (900)
|
||||
# and CsrfFilter (1100), and it short-circuits the preflight before authorization
|
||||
# ever sees it. Note Access-Control-Max-Age: 1800 - that default comes from MVC's
|
||||
# CorsRegistration, not from CorsConfiguration.
|
||||
36
cors-csrf/docs/output/03-security-source.txt
Normal file
36
cors-csrf/docs/output/03-security-source.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
==============================================================================
|
||||
docs/output/03-security-source.txt
|
||||
A @Bean named corsConfigurationSource. .cors(..) is never called - it is applied for us.
|
||||
Profile: securitysource
|
||||
==============================================================================
|
||||
|
||||
$ curl -s localhost:8080/diag/cors-sources
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"corsConfigurationSource": "UrlBasedCorsConfigurationSource",
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": true
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Methods: GET,POST
|
||||
Access-Control-Allow-Headers: content-type, x-xsrf-token
|
||||
Access-Control-Allow-Credentials: true
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# Compare with 02: there is no Access-Control-Max-Age here. CorsConfiguration
|
||||
# leaves maxAge null, so every single cross-origin call re-runs the preflight.
|
||||
47
cors-csrf/docs/output/04-three-identical-403s.txt
Normal file
47
cors-csrf/docs/output/04-three-identical-403s.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
==============================================================================
|
||||
docs/output/04-three-identical-403s.txt
|
||||
Origin not allowed, method not allowed, header not allowed. One status, one shape.
|
||||
Profile: securitysource, CORS_LOG_LEVEL=DEBUG
|
||||
==============================================================================
|
||||
|
||||
# 1. disallowed origin
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# 2. disallowed method
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# 3. disallowed request header
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# body of a rejected preflight:
|
||||
Invalid CORS request
|
||||
|
||||
# The only thing that distinguishes them is a DEBUG line from DefaultCorsProcessor:
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: HTTP 'DELETE' is not allowed
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: headers '[authorization]' are not allowed
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: 'https://evil.example.com' origin is not allowed
|
||||
39
cors-csrf/docs/output/05-misnamed-bean.txt
Normal file
39
cors-csrf/docs/output/05-misnamed-bean.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
==============================================================================
|
||||
docs/output/05-misnamed-bean.txt
|
||||
The same UrlBasedCorsConfigurationSource bean, named apiCorsSource instead of
|
||||
corsConfigurationSource. It starts. The preflight returns 200. It carries no CORS headers.
|
||||
Profile: misnamed
|
||||
==============================================================================
|
||||
|
||||
$ curl -s localhost:8080/diag/cors-sources
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"apiCorsSource": "UrlBasedCorsConfigurationSource",
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": false
|
||||
}
|
||||
|
||||
$ curl -s -i -X OPTIONS http://localhost:8080/api/data \
|
||||
-H 'Origin: https://spa.example.com' \
|
||||
-H 'Access-Control-Request-Method: POST' \
|
||||
-H 'Access-Control-Request-Headers: content-type,x-xsrf-token'
|
||||
|
||||
HTTP/1.1 200
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Skip: no CORS configuration has been provided
|
||||
|
||||
# Two different lookups. HttpSecurityConfiguration.applyCorsIfAvailable asks
|
||||
# getBeanNamesForType(UrlBasedCorsConfigurationSource.class) and enables CORS if the
|
||||
# array is non-empty, so the bean above DID switch the configurer on.
|
||||
# CorsConfigurer.getCorsConfigurationSource then asks
|
||||
# containsBeanDefinition("corsConfigurationSource"), which is false, and falls back
|
||||
# to Spring MVC's registrations - of which there are none.
|
||||
# CorsFilter returns from every preflight whether or not it found a configuration:
|
||||
# if (!isValid || CorsUtils.isPreFlightRequest(request)) { return; }
|
||||
# so the OPTIONS never reaches AuthorizationFilter and the client gets a bare 200.
|
||||
44
cors-csrf/docs/output/06-two-sources.txt
Normal file
44
cors-csrf/docs/output/06-two-sources.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
==============================================================================
|
||||
docs/output/06-two-sources.txt
|
||||
Two UrlBasedCorsConfigurationSource beans. The reference documentation says Spring Security
|
||||
'won't automatically configure CORS support for you, because it cannot decide which one to
|
||||
use'. In 7.1.1 it configures it, and the bean NAME decides.
|
||||
Profile: twosources
|
||||
==============================================================================
|
||||
|
||||
{
|
||||
"corsConfigurationSourceBeans": {
|
||||
"corsConfigurationSource": "UrlBasedCorsConfigurationSource",
|
||||
"adminCorsSource": "UrlBasedCorsConfigurationSource",
|
||||
"mvcHandlerMappingIntrospector": "HandlerMappingIntrospector"
|
||||
},
|
||||
"hasBeanNamedCorsConfigurationSource": true
|
||||
}
|
||||
|
||||
# the origin allowed by the bean named corsConfigurationSource:
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Access-Control-Allow-Origin: https://spa.example.com
|
||||
Access-Control-Allow-Methods: GET,POST
|
||||
Access-Control-Allow-Headers: content-type
|
||||
Access-Control-Allow-Credentials: true
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# the origin allowed by adminCorsSource, which is never consulted:
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.web.cors.DefaultCorsProcessor : Reject: 'https://admin.example.com' origin is not allowed
|
||||
41
cors-csrf/docs/output/07-wildcard-credentials.txt
Normal file
41
cors-csrf/docs/output/07-wildcard-credentials.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
==============================================================================
|
||||
docs/output/07-wildcard-credentials.txt
|
||||
allowedOrigins("*") together with allowCredentials(true). Legal to configure, illegal to
|
||||
serve. The failure is thrown on the request, not at startup - and it does not surface as a 500.
|
||||
Profile: wildcard
|
||||
==============================================================================
|
||||
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
# and a plain authenticated GET, with correct credentials:
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
|
||||
at org.springframework.web.cors.CorsConfiguration.validateAllowCredentials(CorsConfiguration.java:552) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
at org.springframework.web.cors.CorsConfiguration.checkOrigin(CorsConfiguration.java:678) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
at org.springframework.web.cors.DefaultCorsProcessor.checkOrigin(DefaultCorsProcessor.java:193) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
at org.springframework.web.cors.DefaultCorsProcessor.handleInternal(DefaultCorsProcessor.java:131) ~[spring-web-7.0.9.jar:7.0.9]
|
||||
|
||||
# 401, not 500. The exception escapes CorsFilter, Tomcat re-dispatches to /error,
|
||||
# the security chain runs again on that dispatch without re-reading the credential,
|
||||
# and the anonymous second pass is what answers.
|
||||
54
cors-csrf/docs/output/08-csrf-naive.txt
Normal file
54
cors-csrf/docs/output/08-csrf-naive.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
==============================================================================
|
||||
docs/output/08-csrf-naive.txt
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse() on its own - the recipe from every pre-6.0
|
||||
tutorial. Three separate things go wrong.
|
||||
Profile: csrfnaive
|
||||
==============================================================================
|
||||
|
||||
# 1. The bootstrap GET. A SPA expects an XSRF-TOKEN cookie here.
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# cookie jar after the GET:
|
||||
(empty - no cookie was set)
|
||||
|
||||
# 2. POST with no token.
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
# cookie jar now:
|
||||
|
||||
localhost | FALSE | / | FALSE | 0 | XSRF-TOKEN | 4888debb-2e51-4742-b0e7-262c489825b9
|
||||
|
||||
# 3. POST echoing the raw cookie value back in X-XSRF-TOKEN, which is what every
|
||||
# SPA snippet on the internet does.
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/data
|
||||
<timestamp> DEBUG <pid> --- [cors-csrf-samesite] [nio-8080-exec-N] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/data
|
||||
25
cors-csrf/docs/output/09-error-dispatch.txt
Normal file
25
cors-csrf/docs/output/09-error-dispatch.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
==============================================================================
|
||||
docs/output/09-error-dispatch.txt
|
||||
The identical CSRF failure, with one extra filter chain that permits /error.
|
||||
Profile: csrfnaive,errorpermit
|
||||
==============================================================================
|
||||
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
{"timestamp":"<timestamp>","status":403,"error":"Forbidden","path":"/api/data"}
|
||||
|
||||
# 403, and a body. Without the /error chain the same request answers 401 with an
|
||||
# empty body and a WWW-Authenticate header - see 08. AccessDeniedHandlerImpl calls
|
||||
# response.sendError(403), the container re-dispatches to /error, and the security
|
||||
# chain runs a second time on that dispatch. BasicAuthenticationFilter extends
|
||||
# OncePerRequestFilter and skips error dispatches, so the second pass is anonymous
|
||||
# and AuthorizationFilter answers 401 over the top of the 403.
|
||||
47
cors-csrf/docs/output/10-csrf-spa.txt
Normal file
47
cors-csrf/docs/output/10-csrf-spa.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
==============================================================================
|
||||
docs/output/10-csrf-spa.txt
|
||||
The same flow under csrf.spa(), added in Spring Security 7.0.
|
||||
Profile: csrfspa
|
||||
==============================================================================
|
||||
|
||||
# 1. The bootstrap GET now DOES set the cookie.
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# 2. POST with no token still fails, as it must.
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
|
||||
# 3. POST echoing the raw cookie value in X-XSRF-TOKEN.
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
{"created":true,"received":"{}","cookies":"XSRF-TOKEN"}
|
||||
|
||||
# Note the cookie attributes: Path=/ and nothing else. No SameSite, no Secure,
|
||||
# no HttpOnly. A cookie with no SameSite attribute is treated as Lax, so a
|
||||
# genuinely cross-site SPA still never receives it. See 12.
|
||||
24
cors-csrf/docs/output/11-spa-ordering.txt
Normal file
24
cors-csrf/docs/output/11-spa-ordering.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
==============================================================================
|
||||
docs/output/11-spa-ordering.txt
|
||||
.csrf(c -> c.csrfTokenRepository(custom).spa()) - the custom repository asked for a cookie
|
||||
named MY-CSRF and a header named X-CSRF-TOKEN. Neither reaches the running application.
|
||||
Profile: spaorder
|
||||
==============================================================================
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# cookie jar:
|
||||
|
||||
localhost | FALSE | / | FALSE | 0 | XSRF-TOKEN | 42573eea-76d6-4bc6-a14b-bff76640461d
|
||||
|
||||
# spa() assigns csrfTokenRepository and requestHandler unconditionally; it is not a
|
||||
# 'defaults if unset' method. Swap the two calls and MY-CSRF appears.
|
||||
59
cors-csrf/docs/output/12-samesite.txt
Normal file
59
cors-csrf/docs/output/12-samesite.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
==============================================================================
|
||||
docs/output/12-samesite.txt
|
||||
The Set-Cookie headers this application emits under four configurations, and what
|
||||
SpecCookieJar - a model of RFC 6265bis 5.5 and 5.8.3 - does with them.
|
||||
==============================================================================
|
||||
|
||||
## csrf.spa() defaults, session cookie left at same-site=lax
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
|
||||
|
||||
## session cookie set to same-site=none, secure=false
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=None
|
||||
|
||||
## crosssite profile: SameSite=None and Secure on both cookies
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/; Secure; SameSite=None
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; Secure; HttpOnly; SameSite=None
|
||||
|
||||
## crosssite profile with -DOMIT_SECURE=true
|
||||
Set-Cookie: XSRF-TOKEN=<token>; Path=/; SameSite=None
|
||||
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=None
|
||||
|
||||
## The same headers, run through SpecCookieJar
|
||||
{
|
||||
"origin": "not trustworthy (plain http)",
|
||||
"setCookieOutcomes": {
|
||||
"JSESSIONID=s1; Path=/; HttpOnly; SameSite=Lax": "stored",
|
||||
"JSESSIONID=s2; Path=/; HttpOnly; SameSite=None": "REJECTED JSESSIONID: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"JSESSIONID=s3; Path=/; Secure; HttpOnly; SameSite=None": "REJECTED JSESSIONID: SameSite=None with Secure, but the origin is not trustworthy so Secure is not honoured - RFC 6265bis 5.5",
|
||||
"XSRF-TOKEN=t1; Path=/": "stored",
|
||||
"XSRF-TOKEN=t2; Path=/; SameSite=None": "REJECTED XSRF-TOKEN: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"XSRF-TOKEN=t3; Path=/; Secure; SameSite=None": "REJECTED XSRF-TOKEN: SameSite=None with Secure, but the origin is not trustworthy so Secure is not honoured - RFC 6265bis 5.5"
|
||||
},
|
||||
"sentOnSameSiteRequest": "JSESSIONID=s1; XSRF-TOKEN=t1",
|
||||
"sentOnCrossSiteTopLevelNavigation": "JSESSIONID=s1; XSRF-TOKEN=t1",
|
||||
"sentOnCrossSiteFetch": "(no cookies sent)"
|
||||
}
|
||||
|
||||
{
|
||||
"origin": "trustworthy (https, or http://localhost)",
|
||||
"setCookieOutcomes": {
|
||||
"JSESSIONID=s1; Path=/; HttpOnly; SameSite=Lax": "stored",
|
||||
"JSESSIONID=s2; Path=/; HttpOnly; SameSite=None": "REJECTED JSESSIONID: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"JSESSIONID=s3; Path=/; Secure; HttpOnly; SameSite=None": "stored",
|
||||
"XSRF-TOKEN=t1; Path=/": "stored",
|
||||
"XSRF-TOKEN=t2; Path=/; SameSite=None": "REJECTED XSRF-TOKEN: SameSite=None and no Secure attribute - RFC 6265bis 5.5",
|
||||
"XSRF-TOKEN=t3; Path=/; Secure; SameSite=None": "stored"
|
||||
},
|
||||
"sentOnSameSiteRequest": "JSESSIONID=s3; XSRF-TOKEN=t3",
|
||||
"sentOnCrossSiteTopLevelNavigation": "JSESSIONID=s3; XSRF-TOKEN=t3",
|
||||
"sentOnCrossSiteFetch": "JSESSIONID=s3; XSRF-TOKEN=t3"
|
||||
}
|
||||
|
||||
# Read the second block first: over a trustworthy origin, the only two of the six
|
||||
# that reach a cross-site fetch are the two carrying Secure AND SameSite=None.
|
||||
# Then read the first: over plain http, none do -
|
||||
# which is why a cross-site SPA cannot be developed against http://127.0.0.1.
|
||||
# (http://localhost itself is treated as trustworthy by current browsers; a bare IP
|
||||
# is not.)
|
||||
34
cors-csrf/docs/output/13-tests.txt
Normal file
34
cors-csrf/docs/output/13-tests.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
==============================================================================
|
||||
docs/output/13-tests.txt
|
||||
mvn -B test
|
||||
==============================================================================
|
||||
|
||||
09:33:03.378 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Naive
|
||||
09:33:03.464 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Naive
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.904 s -- in CookieCsrfTokenRepository.withHttpOnlyFalse() on its own
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Spa
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Spa
|
||||
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.444 s -- in csrf.spa()
|
||||
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 s -- in SpecCookieJar - the storage and sending rules a browser applies
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Ordering
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CsrfAndCookieTests$Ordering
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.362 s -- in csrfTokenRepository(..) before spa()
|
||||
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.783 s -- in com.ankurm.cors.CsrfAndCookieTests
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcOnly
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcOnly
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.305 s -- in CORS on the MVC layer only
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$Misnamed
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$Misnamed
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.264 s -- in the right type under the wrong bean name
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$SecuritySource
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$SecuritySource
|
||||
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.225 s -- in a bean named corsConfigurationSource
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$TwoSources
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$TwoSources
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.235 s -- in two UrlBasedCorsConfigurationSource beans
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcBridge
|
||||
<timestamp> INFO <pid> --- [cors-csrf-samesite] [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.cors.CorsCsrfApplication for test class com.ankurm.cors.CorsContractTests$MvcBridge
|
||||
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.217 s -- in the same MVC configuration plus .cors(withDefaults())
|
||||
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.256 s -- in com.ankurm.cors.CorsContractTests
|
||||
[INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
Reference in New Issue
Block a user