From 09d9450154be796349dabdc606ade57beae08724 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 14 Jun 2018 11:22:43 +0200 Subject: [PATCH] Add SameSite support in WebFlux SESSION cookies This commit adds support for the "SameSite" attribute in response cookies. As explained in rfc6265bis, this attribute can be used to limit the scope of a cookie so that it can't be attached to a request unless it is sent from the "same-site". This feature is currently supported by Google Chrome and Firefox, other browsers will ignore this attribute. This feature can help prevent CSRF attacks; this is why this commit adds this attribute by default for SESSION Cookies in WebFlux. See: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis Issue: SPR-16418 --- .../springframework/http/ResponseCookie.java | 40 +++++++++++++++++-- .../session/CookieWebSessionIdResolver.java | 28 +++++++++++-- .../CookieWebSessionIdResolverTests.java | 4 +- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java index b459e3a147a..6c44f596ca3 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.util.StringUtils; * static method. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 * @see RFC 6265 */ @@ -46,12 +47,15 @@ public final class ResponseCookie extends HttpCookie { private final boolean httpOnly; + @Nullable + private final String sameSite; + /** * Private constructor. See {@link #from(String, String)}. */ private ResponseCookie(String name, String value, Duration maxAge, @Nullable String domain, - @Nullable String path, boolean secure, boolean httpOnly) { + @Nullable String path, boolean secure, boolean httpOnly, @Nullable String sameSite) { super(name, value); Assert.notNull(maxAge, "Max age must not be null"); @@ -60,6 +64,7 @@ public final class ResponseCookie extends HttpCookie { this.path = path; this.secure = secure; this.httpOnly = httpOnly; + this.sameSite = sameSite; } @@ -105,6 +110,16 @@ public final class ResponseCookie extends HttpCookie { return this.httpOnly; } + /** + * Return the cookie "SameSite" attribute, or {@code null} if not set. + *

This limits the scope of the cookie such that it will only be attached to + * same site requests if {@code "Strict"} or cross-site requests if {@code "Lax"}. + * @see RFC6265 bis + */ + @Nullable + public String getSameSite() { + return this.sameSite; + } @Override public boolean equals(Object other) { @@ -146,13 +161,15 @@ public final class ResponseCookie extends HttpCookie { headers.setExpires(seconds > 0 ? System.currentTimeMillis() + seconds : 0); sb.append(headers.getFirst(HttpHeaders.EXPIRES)); } - if (this.secure) { sb.append("; Secure"); } if (this.httpOnly) { sb.append("; HttpOnly"); } + if (StringUtils.hasText(this.sameSite)) { + sb.append("; SameSite=").append(this.sameSite); + } return sb.toString(); } @@ -180,6 +197,9 @@ public final class ResponseCookie extends HttpCookie { private boolean httpOnly; + @Nullable + private String sameSite; + @Override public ResponseCookieBuilder maxAge(Duration maxAge) { this.maxAge = maxAge; @@ -216,10 +236,16 @@ public final class ResponseCookie extends HttpCookie { return this; } + @Override + public ResponseCookieBuilder sameSite(String sameSite) { + this.sameSite = sameSite; + return this; + } + @Override public ResponseCookie build() { return new ResponseCookie(name, value, this.maxAge, this.domain, this.path, - this.secure, this.httpOnly); + this.secure, this.httpOnly, this.sameSite); } }; } @@ -266,6 +292,12 @@ public final class ResponseCookie extends HttpCookie { */ ResponseCookieBuilder httpOnly(boolean httpOnly); + /** + * Add the "SameSite" attribute to the cookie. + * @see RFC6265 bis + */ + ResponseCookieBuilder sameSite(String sameSite); + /** * Create the HttpCookie. */ diff --git a/spring-web/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index c880504ae6e..809552bfdb0 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -31,6 +31,7 @@ import org.springframework.web.server.ServerWebExchange; * Cookie-based {@link WebSessionIdResolver}. * * @author Rossen Stoyanchev + * @author Brian Clozel * @since 5.0 */ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @@ -39,6 +40,8 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { private Duration cookieMaxAge = Duration.ofSeconds(-1); + private String sameSite = "Strict"; + /** * Set the name of the cookie to use for the session id. @@ -74,6 +77,23 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { return this.cookieMaxAge; } + /** + * Set the value for the "SameSite" attribute of the cookie that holds the + * session id. For its meaning and possible values, see + * {@link ResponseCookie#getSameSite()}. + *

By default set to {@code "Strict"} + * @param sameSite the SameSite value + */ + public void setSameSite(String sameSite) { + this.sameSite = sameSite; + } + + /** + * Return the configured "SameSite" attribute value for the session cookie. + */ + public String getSameSite() { + return sameSite; + } @Override public List resolveSessionIds(ServerWebExchange exchange) { @@ -88,21 +108,21 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override public void setSessionId(ServerWebExchange exchange, String id) { Assert.notNull(id, "'id' is required"); - setSessionCookie(exchange, id, getCookieMaxAge()); + setSessionCookie(exchange, id, getCookieMaxAge(), getSameSite()); } @Override public void expireSession(ServerWebExchange exchange) { - setSessionCookie(exchange, "", Duration.ofSeconds(0)); + setSessionCookie(exchange, "", Duration.ofSeconds(0), ""); } - private void setSessionCookie(ServerWebExchange exchange, String id, Duration maxAge) { + private void setSessionCookie(ServerWebExchange exchange, String id, Duration maxAge, String sameSite) { String name = getCookieName(); boolean secure = "https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()); String path = exchange.getRequest().getPath().contextPath().value() + "/"; exchange.getResponse().getCookies().set(name, ResponseCookie.from(name, id).path(path) - .maxAge(maxAge).httpOnly(true).secure(secure).build()); + .maxAge(maxAge).httpOnly(true).secure(secure).sameSite(sameSite).build()); } } diff --git a/spring-web/src/test/java/org/springframework/web/server/session/CookieWebSessionIdResolverTests.java b/spring-web/src/test/java/org/springframework/web/server/session/CookieWebSessionIdResolverTests.java index f9268239922..47a41495a21 100644 --- a/spring-web/src/test/java/org/springframework/web/server/session/CookieWebSessionIdResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/session/CookieWebSessionIdResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,6 @@ public class CookieWebSessionIdResolverTests { assertEquals(1, cookies.size()); ResponseCookie cookie = cookies.getFirst(this.resolver.getCookieName()); assertNotNull(cookie); - assertEquals("SESSION=123; Path=/; Secure; HttpOnly", cookie.toString()); + assertEquals("SESSION=123; Path=/; Secure; HttpOnly; SameSite=Strict", cookie.toString()); } }