|
| 1 | +[[webflux-csrf]] |
| 2 | += Cross Site Request Forgery (CSRF) for WebFlux Environments |
| 3 | + |
| 4 | +This section discusses Spring Security's <<csrf,Cross Site Request Forgery (CSRF)>> support for WebFlux environments. |
| 5 | + |
| 6 | +[[webflux-csrf-using]] |
| 7 | +== Using Spring Security CSRF Protection |
| 8 | +The steps to using Spring Security's CSRF protection are outlined below: |
| 9 | + |
| 10 | +* <<webflux-csrf-idempotent,Use proper HTTP verbs>> |
| 11 | +* <<webflux-csrf-configure,Configure CSRF Protection>> |
| 12 | +* <<webflux-csrf-include,Include the CSRF Token>> |
| 13 | + |
| 14 | +[[webflux-csrf-idempotent]] |
| 15 | +=== Use proper HTTP verbs |
| 16 | +The first step to protecting against CSRF attacks is to ensure your website uses proper HTTP verbs. |
| 17 | +This is covered in detail in <<csrf-protection-idempotent,Safe Methods Must be Idempotent>>. |
| 18 | + |
| 19 | +[[webflux-csrf-configure]] |
| 20 | +=== Configure CSRF Protection |
| 21 | +The next step is to configure Spring Security's CSRF protection within your application. |
| 22 | +Spring Security's CSRF protection is enabled by default, but you may need to customize the configuration. |
| 23 | +Below are a few common customizations. |
| 24 | + |
| 25 | +[[webflux-csrf-configure-custom-repository]] |
| 26 | +==== Custom CsrfTokenRepository |
| 27 | + |
| 28 | +By default Spring Security stores the expected CSRF token in the `WebSession` using `WebSessionServerCsrfTokenRepository`. |
| 29 | +There can be cases where users will want to configure a custom `ServerCsrfTokenRepository`. |
| 30 | +For example, it might be desirable to persist the `CsrfToken` in a cookie to <<webflux-csrf-include-ajax-auto,support a JavaScript based application>>. |
| 31 | + |
| 32 | +By default the `CookieServerCsrfTokenRepository` will write to a cookie named `XSRF-TOKEN` and read it from a header named `X-XSRF-TOKEN` or the HTTP parameter `_csrf`. |
| 33 | +These defaults come from https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection[AngularJS] |
| 34 | + |
| 35 | +You can configure `CookieCsrfTokenRepository` in Java Configuration using: |
| 36 | + |
| 37 | +.Store CSRF Token in a Cookie with Java Configuration |
| 38 | +==== |
| 39 | +[source,java] |
| 40 | +----- |
| 41 | +@Bean |
| 42 | +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { |
| 43 | + http |
| 44 | + // ... |
| 45 | + .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())) |
| 46 | + return http.build(); |
| 47 | +} |
| 48 | +----- |
| 49 | +==== |
| 50 | + |
| 51 | +[NOTE] |
| 52 | +==== |
| 53 | +The sample explicitly sets `cookieHttpOnly=false`. |
| 54 | +This is necessary to allow JavaScript (i.e. AngularJS) to read it. |
| 55 | +If you do not need the ability to read the cookie with JavaScript directly, it is recommended to omit `cookieHttpOnly=false` (by using `new CookieServerCsrfTokenRepository()` instead) to improve security. |
| 56 | +==== |
| 57 | + |
| 58 | +[[webflux-csrf-configure-disable]] |
| 59 | +==== Disable CSRF Protection |
| 60 | +CSRF protection is enabled by default. |
| 61 | +However, it is simple to disable CSRF protection if it <<csrf-when,makes sense for your application>>. |
| 62 | + |
| 63 | +The Java configuration below will disable CSRF protection. |
| 64 | + |
| 65 | +.Disable CSRF Java Configuration |
| 66 | +==== |
| 67 | +[source,java] |
| 68 | +---- |
| 69 | +@Bean |
| 70 | +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { |
| 71 | + http |
| 72 | + // ... |
| 73 | + .csrf(csrf -> csrf.disable())) |
| 74 | + return http.build(); |
| 75 | +} |
| 76 | +---- |
| 77 | +==== |
| 78 | + |
| 79 | +[[webflux-csrf-include]] |
| 80 | +=== Include the CSRF Token |
| 81 | + |
| 82 | +In order for the <<csrf-protection-stp,synchronizer token pattern>> to protect against CSRF attacks, we must include the actual CSRF token in the HTTP request. |
| 83 | +This must be included in a part of the request (i.e. form parameter, HTTP header, etc) that is not automatically included in the HTTP request by the browser. |
| 84 | + |
| 85 | +Spring Security's https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html[CsrfWebFilter] exposes a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html[Mono<CsrfToken>] as a `ServerWebExchange` attribute named `org.springframework.security.web.server.csrf.CsrfToken`. |
| 86 | +This means that any view technology can access the `Mono<CsrfToken>` to expose the expected token as either a <<webflux-csrf-include-form-attr,form>> or <<webflux-csrf-include-ajax-meta-attr,meta tag>>. |
| 87 | + |
| 88 | +[[webflux-csrf-include-subscribe]] |
| 89 | +If your view technology does not provide a simple way to subscribe to the `Mono<CsrfToken>`, a common pattern is to use Spring's `@ControllerAdvice` to expose the `CsrfToken` directly. |
| 90 | +For example, the following code will place the `CsrfToken` on the default attribute name (`_csrf`) used by Spring Security's <<webflux-csrf-include-form-auto,CsrfRequestDataValueProcessor>> to automatically include the CSRF token as a hidden input. |
| 91 | + |
| 92 | +.`CsrfToken` as `@ModelAttribute` |
| 93 | +==== |
| 94 | +[source,java] |
| 95 | +---- |
| 96 | +@ControllerAdvice |
| 97 | +public class SecurityControllerAdvice { |
| 98 | + @ModelAttribute |
| 99 | + Mono<CsrfToken> csrfToken(ServerWebExchange exchange) { |
| 100 | + Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName()); |
| 101 | + return csrfToken.doOnSuccess(token -> exchange.getAttributes() |
| 102 | + .put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token)); |
| 103 | + } |
| 104 | +} |
| 105 | +---- |
| 106 | +==== |
| 107 | + |
| 108 | +Fortunately, Thymeleaf provides <<webflux-csrf-include-form-auto,integration>> that works without any additional work. |
| 109 | + |
| 110 | +[[webflux-csrf-include-form]] |
| 111 | +==== Form URL Encoded |
| 112 | +In order to post an HTML form the CSRF token must be included in the form as a hidden input. |
| 113 | +For example, the rendered HTML might look like: |
| 114 | + |
| 115 | +.CSRF Token HTML |
| 116 | +==== |
| 117 | +[source,html] |
| 118 | +---- |
| 119 | +<input type="hidden" |
| 120 | + name="_csrf" |
| 121 | + value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/> |
| 122 | +---- |
| 123 | +==== |
| 124 | + |
| 125 | +Next we will discuss various ways of including the CSRF token in a form as a hidden input. |
| 126 | + |
| 127 | +[[webflux-csrf-include-form-auto]] |
| 128 | +===== Automatic CSRF Token Inclusion |
| 129 | + |
| 130 | +Spring Security's CSRF support provides integration with Spring's https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/result/view/RequestDataValueProcessor.html[RequestDataValueProcessor] via its https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html[CsrfRequestDataValueProcessor]. |
| 131 | +In order for `CsrfRequestDataValueProcessor` to work, the `Mono<CsrfToken>` must be subscribed to and the `CsrfToken` must be <<webflux-csrf-include-subscribe,exposed as an attribute>> that matches https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html#DEFAULT_CSRF_ATTR_NAME[DEFAULT_CSRF_ATTR_NAME]. |
| 132 | + |
| 133 | +Fortunately, Thymleaf https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor[provides support] to take care of all the boilerplate for you by integrating with `RequestDataValueProcessor` to ensure that forms that have an unsafe HTTP method (i.e. post) will automatically include the actual CSRF token. |
| 134 | + |
| 135 | +[[webflux-csrf-include-form-attr]] |
| 136 | +===== CsrfToken Request Attribute |
| 137 | + |
| 138 | +If the <<webflux-csrf-include,other options>> for including the actual CSRF token in the request do not work, you can take advantage of the fact that the `Mono<CsrfToken>` <<webflux-csrf-include,is exposed>> as a `ServerWebExchange` attribute named `org.springframework.security.web.server.csrf.CsrfToken`. |
| 139 | + |
| 140 | +The Thymeleaf sample below assumes that you <<webflux-csrf-include-subscribe,expose>> the `CsrfToken` on an attribute named `_csrf`. |
| 141 | + |
| 142 | +.CSRF Token in Form with Request Attribute |
| 143 | +==== |
| 144 | +[source,html] |
| 145 | +---- |
| 146 | +<form th:action="@{/logout}" |
| 147 | + method="post"> |
| 148 | +<input type="submit" |
| 149 | + value="Log out" /> |
| 150 | +<input type="hidden" |
| 151 | + th:name="${_csrf.parameterName}" |
| 152 | + th:value="${_csrf.token}"/> |
| 153 | +</form> |
| 154 | +---- |
| 155 | +==== |
| 156 | + |
| 157 | +[[webflux-csrf-include-ajax]] |
| 158 | +==== Ajax and JSON Requests |
| 159 | +If you are using JSON, then it is not possible to submit the CSRF token within an HTTP parameter. |
| 160 | +Instead you can submit the token within a HTTP header. |
| 161 | + |
| 162 | +In the following sections we will discuss various ways of including the CSRF token as an HTTP request header in JavaScript based applications. |
| 163 | + |
| 164 | +[[webflux-csrf-include-ajax-auto]] |
| 165 | +===== Automatic Inclusion |
| 166 | + |
| 167 | +Spring Security can easily be <<webflux-csrf-configure-custom-repository,configured>> to store the expected CSRF token in a cookie. |
| 168 | +By storing the expected CSRF in a cookie, JavaScript frameworks like https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection[AngularJS] will automatically include the actual CSRF token in the HTTP request headers. |
| 169 | + |
| 170 | +[[webflux-csrf-include-ajax-meta]] |
| 171 | +===== Meta tags |
| 172 | + |
| 173 | +An alternative pattern to <<webflux-csrf-include-form-auto,exposing the CSRF in a cookie>> is to include the CSRF token within your `meta` tags. |
| 174 | +The HTML might look something like this: |
| 175 | + |
| 176 | +.CSRF meta tag HTML |
| 177 | +==== |
| 178 | +[source,html] |
| 179 | +---- |
| 180 | +<html> |
| 181 | +<head> |
| 182 | + <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/> |
| 183 | + <meta name="_csrf_header" content="X-CSRF-TOKEN"/> |
| 184 | + <!-- ... --> |
| 185 | +</head> |
| 186 | +<!-- ... --> |
| 187 | +---- |
| 188 | +==== |
| 189 | + |
| 190 | +Once the meta tags contained the CSRF token, the JavaScript code would read the meta tags and include the CSRF token as a header. |
| 191 | +If you were using jQuery, this could be done with the following: |
| 192 | + |
| 193 | +.AJAX send CSRF Token |
| 194 | +==== |
| 195 | +[source,javascript] |
| 196 | +---- |
| 197 | +$(function () { |
| 198 | + var token = $("meta[name='_csrf']").attr("content"); |
| 199 | + var header = $("meta[name='_csrf_header']").attr("content"); |
| 200 | + $(document).ajaxSend(function(e, xhr, options) { |
| 201 | + xhr.setRequestHeader(header, token); |
| 202 | + }); |
| 203 | +}); |
| 204 | +---- |
| 205 | +==== |
| 206 | + |
| 207 | +The sample below assumes that you <<webflux-csrf-include-subscribe,expose>> the `CsrfToken` on an attribute named `_csrf`. |
| 208 | +An example of doing this with Thymeleaf is shown below: |
| 209 | + |
| 210 | +.CSRF meta tag JSP |
| 211 | +==== |
| 212 | +[source,html] |
| 213 | +---- |
| 214 | +<html> |
| 215 | +<head> |
| 216 | + <meta name="_csrf" th:content="${_csrf.token}"/> |
| 217 | + <!-- default header name is X-CSRF-TOKEN --> |
| 218 | + <meta name="_csrf_header" th:content="${_csrf.headerName}"/> |
| 219 | + <!-- ... --> |
| 220 | +</head> |
| 221 | +<!-- ... --> |
| 222 | +---- |
| 223 | +==== |
| 224 | + |
| 225 | +[[webflux-csrf-considerations]] |
| 226 | +== CSRF Considerations |
| 227 | +There are a few special considerations to consider when implementing protection against CSRF attacks. |
| 228 | +This section discusses those considerations as it pertains to WebFlux environments. |
| 229 | +Refer to <<csrf-considerations>> for a more general discussion. |
| 230 | + |
| 231 | + |
| 232 | +[[webflux-considerations-csrf-login]] |
| 233 | +=== Logging In |
| 234 | + |
| 235 | +It is important to <<csrf-considerations-login,require CSRF for log in>> requests to protect against forging log in attempts. |
| 236 | +Spring Security's WebFlux support does this out of the box. |
| 237 | + |
| 238 | +[[webflux-considerations-csrf-logout]] |
| 239 | +=== Logging Out |
| 240 | + |
| 241 | +It is important to <<csrf-considerations-logout,require CSRF for log out>> requests to protect against forging log out attempts. |
| 242 | +By default Spring Security's `LogoutWebFilter` only processes HTTP post requests. |
| 243 | +This ensures that log out requires a CSRF token and that a malicious user cannot forcibly log out your users. |
| 244 | + |
| 245 | +The easiest approach is to use a form to log out. |
| 246 | +If you really want a link, you can use JavaScript to have the link perform a POST (i.e. maybe on a hidden form). |
| 247 | +For browsers with JavaScript that is disabled, you can optionally have the link take the user to a log out confirmation page that will perform the POST. |
| 248 | + |
| 249 | +If you really want to use HTTP GET with logout you can do so, but remember this is generally not recommended. |
| 250 | +For example, the following Java Configuration will perform logout with the URL `/logout` is requested with any HTTP method: |
| 251 | + |
| 252 | +// FIXME: This should be a link to log out documentation |
| 253 | + |
| 254 | +.Log out with HTTP GET |
| 255 | +==== |
| 256 | +[source,java] |
| 257 | +---- |
| 258 | +@Bean |
| 259 | +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { |
| 260 | + http |
| 261 | + // ... |
| 262 | + .logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout"))) |
| 263 | + return http.build(); |
| 264 | +} |
| 265 | +
|
| 266 | +---- |
| 267 | +==== |
| 268 | + |
| 269 | + |
| 270 | +[[webflux-considerations-csrf-timeouts]] |
| 271 | +=== CSRF and Session Timeouts |
| 272 | + |
| 273 | +By default Spring Security stores the CSRF token in the `WebSession`. |
| 274 | +This can lead to a situation where the session expires which means there is not an expected CSRF token to validate against. |
| 275 | + |
| 276 | +We've already discussed <<csrf-considerations-login,general solutions>> to session timeouts. |
| 277 | +This section discusses the specifics of CSRF timeouts as it pertains to the WebFlux support. |
| 278 | + |
| 279 | +It is simple to change storage of the expected CSRF token to be in a cookie. |
| 280 | +For details, refer to the <<webflux-csrf-configure-custom-repository>> section. |
| 281 | + |
| 282 | +// FIXME: We should add a custom AccessDeniedHandler section in the reference and update the links above |
| 283 | + |
| 284 | +// FIXME: We need a WebFlux multipart body vs action story. WebFlux always has multipart enabled. |
| 285 | +[[webflux-csrf-considerations-multipart]] |
| 286 | +=== Multipart (file upload) |
| 287 | +We have <<csrf-considerations-multipart,already discussed>> how protecting multipart requests (file uploads) from CSRF attacks causes a https://en.wikipedia.org/wiki/Chicken_or_the_egg[chicken and the egg] problem. |
| 288 | +This section discusses how to implement placing the CSRF token in the <<webflux-csrf-considerations-multipart-body,body>> and <<webflux-csrf-considerations-multipart-url,url>> within a WebFlux application. |
| 289 | + |
| 290 | +[NOTE] |
| 291 | +==== |
| 292 | +More information about using multipart forms with Spring can be found within the https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web-reactive.html#webflux-multipart[Multipart Data] section of the Spring reference. |
| 293 | +==== |
| 294 | + |
| 295 | +[[webflux-csrf-considerations-multipart-body]] |
| 296 | +==== Place CSRF Token in the Body |
| 297 | + |
| 298 | +We have <<csrf-considerations-multipart,already discussed>> the trade-offs of placing the CSRF token in the body. |
| 299 | + |
| 300 | +In a WebFlux application, this can be configured with the following configuration: |
| 301 | + |
| 302 | +.Enable obtaining CSRF token from multipart/form-data |
| 303 | +==== |
| 304 | +[source,java] |
| 305 | +---- |
| 306 | +@Bean |
| 307 | +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { |
| 308 | + http |
| 309 | + // ... |
| 310 | + .csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true)) |
| 311 | + return http.build(); |
| 312 | +} |
| 313 | +
|
| 314 | +---- |
| 315 | +==== |
| 316 | + |
| 317 | +[[webflux-csrf-considerations-multipart-url]] |
| 318 | +==== Include CSRF Token in URL |
| 319 | + |
| 320 | +We have <<csrf-considerations-multipart,already discussed>> the trade-offs of placing the CSRF token in the URL. |
| 321 | +Since the `CsrfToken` is exposed as an `ServerHttpRequest` <<webflux-csrf-include,request attribute>>, we can use that to create an `action` with the CSRF token in it. |
| 322 | +An example with Thymeleaf is shown below: |
| 323 | + |
| 324 | +.CSRF Token in Action |
| 325 | +==== |
| 326 | +[source,html] |
| 327 | +---- |
| 328 | +<form method="post" |
| 329 | + th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}" |
| 330 | + enctype="multipart/form-data"> |
| 331 | +---- |
| 332 | +==== |
| 333 | + |
| 334 | +[[webflux-csrf-considerations-override-method]] |
| 335 | +=== HiddenHttpMethodFilter |
| 336 | +We have <<csrf-considerations-override-method,already discussed>> overriding the HTTP method. |
| 337 | + |
| 338 | +In a Spring WebFlux application, overriding the HTTP method is done using https://docs.spring.io/spring-framework/docs/5.2.x/javadoc-api/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.html[HiddenHttpMethodFilter]. |
0 commit comments