Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-1328: Added SameSite support for session cookie. #1369

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions framework/src/play/data/validation/ValidationPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ static void save() {
if (Validation.errors().isEmpty()) {
// Only send "delete cookie" header when the cookie was present in the request
if(Http.Request.current().cookies.containsKey(Scope.COOKIE_PREFIX + "_ERRORS") || !Scope.SESSION_SEND_ONLY_IF_CHANGED) {
Http.Response.current().setCookie(Scope.COOKIE_PREFIX + "_ERRORS", "", null, "/", 0, Scope.COOKIE_SECURE, Scope.SESSION_HTTPONLY);
Http.Response.current().setCookie(Scope.COOKIE_PREFIX + "_ERRORS", "", null, "/", 0, Scope.COOKIE_SECURE, Scope.SESSION_HTTPONLY, null);
}
return;
}
Expand All @@ -180,7 +180,7 @@ static void save() {
}
}
String errorsData = URLEncoder.encode(errors.toString(), StandardCharsets.UTF_8);
Http.Response.current().setCookie(Scope.COOKIE_PREFIX + "_ERRORS", errorsData, null, "/", null, Scope.COOKIE_SECURE, Scope.SESSION_HTTPONLY);
Http.Response.current().setCookie(Scope.COOKIE_PREFIX + "_ERRORS", errorsData, null, "/", null, Scope.COOKIE_SECURE, Scope.SESSION_HTTPONLY, null);
} catch (Exception e) {
throw new UnexpectedException("Errors serializationProblem", e);
}
Expand Down
4 changes: 2 additions & 2 deletions framework/src/play/i18n/Lang.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public static void change(String locale) {
Response response = Response.current();
if (response != null) {
// We have a current response in scope - set the language-cookie to store the selected language for the next requests
response.setCookie(Play.configuration.getProperty("application.lang.cookie", "PLAY_LANG"), locale, null, "/", null, Scope.COOKIE_SECURE);
response.setCookie(Play.configuration.getProperty("application.lang.cookie", "PLAY_LANG"), locale, null, "/", null, Scope.COOKIE_SECURE, Scope.SESSION_SAMESITE);
}
}

Expand Down Expand Up @@ -155,7 +155,7 @@ private static void resolveFrom(Request request) {
return;
}
// could not use locale from cookie - clear the locale-cookie
Response.current().setCookie(cn, "", null, "/", null, Scope.COOKIE_SECURE);
Response.current().setCookie(cn, "", null, "/", null, Scope.COOKIE_SECURE, Scope.SESSION_SAMESITE);

}

Expand Down
6 changes: 3 additions & 3 deletions framework/src/play/mvc/CookieSessionStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void save(Session session) {
if (session.isEmpty()) {
// The session is empty: delete the cookie
if (Http.Request.current().cookies.containsKey(COOKIE_PREFIX + "_SESSION") || !SESSION_SEND_ONLY_IF_CHANGED) {
Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", "", null, "/", 0, COOKIE_SECURE, SESSION_HTTPONLY);
Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", "", null, "/", 0, COOKIE_SECURE, SESSION_HTTPONLY, SESSION_SAMESITE);
}
return;
}
Expand All @@ -84,10 +84,10 @@ public void save(Session session) {
String sign = Crypto.sign(sessionData, Play.secretKey.getBytes());
if (COOKIE_EXPIRE == null) {
Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", sign + "-" + sessionData, null, "/", null, COOKIE_SECURE,
SESSION_HTTPONLY);
SESSION_HTTPONLY, SESSION_SAMESITE);
} else {
Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", sign + "-" + sessionData, null, "/",
Time.parseDuration(COOKIE_EXPIRE), COOKIE_SECURE, SESSION_HTTPONLY);
Time.parseDuration(COOKIE_EXPIRE), COOKIE_SECURE, SESSION_HTTPONLY, SESSION_SAMESITE);
}
} catch (Exception e) {
throw new UnexpectedException("Session serializationProblem", e);
Expand Down
37 changes: 29 additions & 8 deletions framework/src/play/mvc/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ public static class Cookie implements Serializable {
* See http://www.owasp.org/index.php/HttpOnly
*/
public boolean httpOnly = false;
/**
* See https://owasp.org/www-community/SameSite
*/
public SameSite sameSite;
}

/**
Expand Down Expand Up @@ -701,8 +705,8 @@ public void setContentTypeIfNotSet(String contentType) {
* @param value
* Cookie value
*/
public void setCookie(String name, String value) {
setCookie(name, value, null, "/", null, false);
public void setCookie(String name, String value, SameSite sameSite) {
setCookie(name, value, null, "/", null, false, sameSite);
}

/**
Expand All @@ -724,7 +728,7 @@ public void removeCookie(String name) {
* cookie path
*/
public void removeCookie(String name, String path) {
setCookie(name, "", null, path, 0, false);
setCookie(name, "", null, path, 0, false, null);
}

/**
Expand All @@ -737,15 +741,15 @@ public void removeCookie(String name, String path) {
* @param duration
* the cookie duration (Ex: 3d)
*/
public void setCookie(String name, String value, String duration) {
setCookie(name, value, null, "/", Time.parseDuration(duration), false);
public void setCookie(String name, String value, String duration, SameSite sameSite) {
setCookie(name, value, null, "/", Time.parseDuration(duration), false, sameSite);
}

public void setCookie(String name, String value, String domain, String path, Integer maxAge, boolean secure) {
setCookie(name, value, domain, path, maxAge, secure, false);
public void setCookie(String name, String value, String domain, String path, Integer maxAge, boolean secure, SameSite sameSite) {
setCookie(name, value, domain, path, maxAge, secure, false, sameSite);
}

public void setCookie(String name, String value, String domain, String path, Integer maxAge, boolean secure, boolean httpOnly) {
public void setCookie(String name, String value, String domain, String path, Integer maxAge, boolean secure, boolean httpOnly, SameSite sameSite) {
path = Play.ctxPath + path;
if (cookies.containsKey(name) && cookies.get(name).path.equals(path)
&& ((cookies.get(name).domain == null && domain == null) || (cookies.get(name).domain.equals(domain)))) {
Expand All @@ -759,6 +763,7 @@ public void setCookie(String name, String value, String domain, String path, Int
cookie.path = path;
cookie.secure = secure;
cookie.httpOnly = httpOnly;
cookie.sameSite = sameSite;
if (domain != null) {
cookie.domain = domain;
} else {
Expand Down Expand Up @@ -1012,4 +1017,20 @@ public WebSocketFrame(byte[] data) {

public static class WebSocketClose extends WebSocketEvent {
}

public enum SameSite {
STRICT("Strict"),
LAX("Lax"),
NONE("None");

private final String value;

SameSite(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
}
6 changes: 4 additions & 2 deletions framework/src/play/mvc/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class Scope {
.equals("true");
public static boolean SESSION_SEND_ONLY_IF_CHANGED = Play.configuration
.getProperty("application.session.sendOnlyIfChanged", "false").toLowerCase().equals("true");
public static final Http.SameSite SESSION_SAMESITE = Play.configuration.getProperty("application.session.cookie.sameSite") != null ?
Http.SameSite.valueOf(Play.configuration.getProperty("application.session.cookie.sameSite").toUpperCase()) : null;

public static final SessionStore sessionStore = createSessionStore();

Expand Down Expand Up @@ -78,13 +80,13 @@ void save() {
}
if (out.isEmpty()) {
if (Http.Request.current().cookies.containsKey(COOKIE_PREFIX + "_FLASH") || !SESSION_SEND_ONLY_IF_CHANGED) {
Http.Response.current().setCookie(COOKIE_PREFIX + "_FLASH", "", null, "/", 0, COOKIE_SECURE, SESSION_HTTPONLY);
Http.Response.current().setCookie(COOKIE_PREFIX + "_FLASH", "", null, "/", 0, COOKIE_SECURE, SESSION_HTTPONLY, SESSION_SAMESITE);
}
return;
}
try {
String flashData = CookieDataCodec.encode(out);
Http.Response.current().setCookie(COOKIE_PREFIX + "_FLASH", flashData, null, "/", null, COOKIE_SECURE, SESSION_HTTPONLY);
Http.Response.current().setCookie(COOKIE_PREFIX + "_FLASH", flashData, null, "/", null, COOKIE_SECURE, SESSION_HTTPONLY, SESSION_SAMESITE);
} catch (Exception e) {
throw new UnexpectedException("Flash serializationProblem", e);
}
Expand Down
13 changes: 10 additions & 3 deletions framework/src/play/server/PlayHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,11 @@ protected static void addToResponse(Response response, HttpResponse nettyRespons
c.setMaxAge(cookie.maxAge);
}
c.setHttpOnly(cookie.httpOnly);
nettyResponse.headers().add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
String encodedCookie = ServerCookieEncoder.STRICT.encode(c);
if (cookie.sameSite != null) {
encodedCookie += "; SameSite=" + cookie.sameSite.getValue();
}
nettyResponse.headers().add(SET_COOKIE, encodedCookie);
}

if (!response.headers.containsKey(CACHE_CONTROL) && !response.headers.containsKey(EXPIRES)
Expand Down Expand Up @@ -767,8 +771,11 @@ public static void serve500(Exception e, ChannelHandlerContext ctx, HttpRequest
c.setMaxAge(cookie.maxAge);
}
c.setHttpOnly(cookie.httpOnly);

nettyResponse.headers().add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
String encodedCookie = ServerCookieEncoder.STRICT.encode(c);
if (cookie.sameSite != null) {
encodedCookie += "; SameSite=" + cookie.sameSite.getValue();
}
nettyResponse.headers().add(SET_COOKIE, encodedCookie);
}

} catch (Exception exx) {
Expand Down
22 changes: 20 additions & 2 deletions framework/test-src/play/mvc/HttpResponseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,30 @@ public class HttpResponseTest {
public void verifyDefaultCookieDomain() {
Http.Cookie.defaultDomain = null;
Http.Response response = new Http.Response();
response.setCookie("testCookie", "testValue");
response.setCookie("testCookie", "testValue", null);
assertThat(response.cookies.get("testCookie").domain).isNull();

Http.Cookie.defaultDomain = ".abc.com";
response = new Http.Response();
response.setCookie("testCookie", "testValue");
response.setCookie("testCookie", "testValue", null);
assertThat(response.cookies.get("testCookie").domain).isEqualTo(".abc.com");
}

@Test
public void verifySameSiteCookie() {
Http.Cookie.defaultDomain = null;
Http.Response response = new Http.Response();
response.setCookie("testCookie", "testValue", null);
assertThat(response.cookies.get("testCookie").sameSite).isNull();

Http.Cookie.defaultDomain = ".abc.com";
response = new Http.Response();
response.setCookie("testCookie", "testValue", Http.SameSite.LAX);
assertThat(response.cookies.get("testCookie").sameSite).isEqualTo(Http.SameSite.LAX);

Http.Cookie.defaultDomain = ".abc.com";
response = new Http.Response();
response.setCookie("testCookie", "testValue", Http.SameSite.STRICT);
assertThat(response.cookies.get("testCookie").sameSite).isEqualTo(Http.SameSite.STRICT);
}
}
2 changes: 1 addition & 1 deletion modules/secure/app/controllers/Secure.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public static void authenticate(@Required String username, String password, bool
Date expiration = new Date();
String duration = Play.configuration.getProperty("secure.rememberme.duration","30d");
expiration.setTime(expiration.getTime() + ((long)Time.parseDuration(duration)) * 1000L );
response.setCookie("rememberme", Crypto.sign(username + "-" + expiration.getTime()) + "-" + username + "-" + expiration.getTime(), duration);
response.setCookie("rememberme", Crypto.sign(username + "-" + expiration.getTime()) + "-" + username + "-" + expiration.getTime(), duration, null);

}
// Redirect to the original URL (or /)
Expand Down
1 change: 1 addition & 0 deletions resources/application-skel/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ date.format=yyyy-MM-dd
# application.session.cookie=PLAY
# application.session.maxAge=1h
# application.session.secure=false
# application.session.cookie.sameSite=lax

# Session/Cookie sharing between subdomain
# ~~~~~~~~~~~~~~~~~~~~~~
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public static void writeChunks2() {

public static void makeSureCookieSaved(){
if(request.cookies!=null && request.cookies.get("PLAY_TEST")!=null){
response.setCookie("PLAY_TEST", request.cookies.get("PLAY_TEST").value);
response.setCookie("PLAY_TEST", request.cookies.get("PLAY_TEST").value, null);
}
renderText("OK");
}
Expand Down