From 0dd5f23e6f2369c39ccf8ba70ff45b8248f8ec26 Mon Sep 17 00:00:00 2001
From: Bruce Toll <btoll@dhsus.com>
Date: Mon, 2 Nov 2020 12:54:31 -0500
Subject: [PATCH] Add SameSite support to setCookie

Provide a sameSite option on setCookie() to provide a mechanism for
explicitly setting a SameSite value to None, Lax, or Strict.

Some browsers log warnings if SameSite is not set (e.g. Firefox 76+).

Default most cookies to have SameSite=Lax, which is (or is expected to
be) the browser default when SameSite is not specified.

Use SameSite::Strict for Wt session id cookies.

Log warning if setCookie() is called with parameters specifying a
non-secure cookie and a SameSite of None, as browsers are supposed
to reject this combination.
---
 src/Wt/WApplication.C    | 10 ++++++----
 src/Wt/WApplication.h    |  6 ++++--
 src/Wt/WGlobal.h         | 10 ++++++++++
 src/web/WebRenderer.C    | 28 +++++++++++++++++++++++-----
 src/web/WebRenderer.h    |  7 +++++--
 src/web/WebSession.C     |  7 ++++---
 src/web/skeleton/Boot.js |  8 ++++----
 7 files changed, 56 insertions(+), 20 deletions(-)

diff --git a/src/Wt/WApplication.C b/src/Wt/WApplication.C
index de5e4497..772cee5a 100644
--- a/src/Wt/WApplication.C
+++ b/src/Wt/WApplication.C
@@ -1085,11 +1085,12 @@ void WApplication::setCookie(const std::string& name,
 			     const std::string& value, int maxAge,
 			     const std::string& domain,
 			     const std::string& path,
-			     bool secure)
+			     bool secure,
+			     CookieSameSite sameSite)
 {
   WDateTime expires = WDateTime::currentDateTime();
   expires = expires.addSecs(maxAge);
-  session_->renderer().setCookie(name, value, expires, domain, path, secure);
+  session_->renderer().setCookie(name, value, expires, domain, path, secure, sameSite);
 }
 
 #ifndef WT_TARGET_JAVA
@@ -1098,9 +1099,10 @@ void WApplication::setCookie(const std::string& name,
 			     const WDateTime& expires,
 			     const std::string& domain,
 			     const std::string& path,
-			     bool secure)
+			     bool secure,
+			     CookieSameSite sameSite)
 {
-  session_->renderer().setCookie(name, value, expires, domain, path, secure);
+  session_->renderer().setCookie(name, value, expires, domain, path, secure, sameSite);
 }
 #endif // WT_TARGET_JAVA
 
diff --git a/src/Wt/WApplication.h b/src/Wt/WApplication.h
index 924de13c..7616fdf4 100644
--- a/src/Wt/WApplication.h
+++ b/src/Wt/WApplication.h
@@ -1657,12 +1657,14 @@ public:
    */
   void setCookie(const std::string& name, const std::string& value,
 		 int maxAge, const std::string& domain = "",
-		 const std::string& path = "", bool secure = false);
+		 const std::string& path = "", bool secure = false,
+		 CookieSameSite sameSite = CookieSameSite::Lax);
 
 #ifndef WT_TARGET_JAVA
   void setCookie(const std::string& name, const std::string& value,
 		 const WDateTime& expires, const std::string& domain = "",
-		 const std::string& path = "", bool secure = false);
+		 const std::string& path = "", bool secure = false,
+		 CookieSameSite sameSite = CookieSameSite::Lax);
 #endif // WT_TARGET_JAVA
 
   /*! \brief Removes a cookie.
diff --git a/src/Wt/WGlobal.h b/src/Wt/WGlobal.h
index 6dbdef9c..758e38eb 100644
--- a/src/Wt/WGlobal.h
+++ b/src/Wt/WGlobal.h
@@ -1106,6 +1106,16 @@ struct AutoCloseable {
 };
 #endif // WT_TARGET_JAVA
 
+/*! \brief Enumeration that specifies the SameSite values for HTTP Cookies
+ *
+ * \sa WApplication::setCookie()
+ */
+enum class CookieSameSite {
+  None,   //!< Sent in all contexts (note: generally not allowed if secure not specified)
+  Lax,    //!< Sent for first-party and top-level GET from third-party
+  Strict  //!< Sent for first-party only
+};
+
 }
 
 #endif // WGLOBALS_H_
diff --git a/src/web/WebRenderer.C b/src/web/WebRenderer.C
index f683d438..8195c253 100644
--- a/src/web/WebRenderer.C
+++ b/src/web/WebRenderer.C
@@ -90,19 +90,22 @@ namespace Wt {
 LOGGER("WebRenderer");
 
 WebRenderer::CookieValue::CookieValue()
-  : secure(false)
+  : secure(false),
+    sameSite(CookieSameSite::Lax)
 { }
 
 WebRenderer::CookieValue::CookieValue(const std::string& v,
 				      const std::string& p,
 				      const std::string& d,
 				      const WDateTime& e,
-				      bool s)
+				      bool s,
+				      CookieSameSite sameSite)
   : value(v),
     path(p),
     domain(d),
     expires(e),
-    secure(s)
+    secure(s),
+    sameSite(sameSite)
 { }
 
 WebRenderer::WebRenderer(WebSession& session)
@@ -505,9 +508,12 @@ void WebRenderer::serveError(int status, WebResponse& response,
 void WebRenderer::setCookie(const std::string name, const std::string value,
 			    const WDateTime& expires,
 			    const std::string domain, const std::string path,
-			    bool secure)
+			    bool secure, CookieSameSite sameSite)
 {
-  cookiesToSet_[name] = CookieValue(value, path, domain, expires, secure);
+  if (sameSite == CookieSameSite::None and !secure) {
+    LOG_WARN("setCookie: called with SameSite::None, but not secure (cookie name: " << name << ")");
+  }
+  cookiesToSet_[name] = CookieValue(value, path, domain, expires, secure, sameSite);
 }
 
 void WebRenderer::setCaching(WebResponse& response, bool allowCache)
@@ -568,6 +574,18 @@ void WebRenderer::setHeaders(WebResponse& response, const std::string mimeType)
     if (cookie.secure)
       header << " secure;";
 
+    switch (cookie.sameSite) {
+    case CookieSameSite::None:
+      header << " SameSite=None;";
+      break;
+    case CookieSameSite::Lax:
+      header << " SameSite=Lax;";
+      break;
+    case CookieSameSite::Strict:
+      header << " SameSite=Strict;";
+      break;
+    }
+
     response.addHeader("Set-Cookie", header.str());
   }
   cookiesToSet_.clear();
diff --git a/src/web/WebRenderer.h b/src/web/WebRenderer.h
index 65c5170c..527c5ba0 100644
--- a/src/web/WebRenderer.h
+++ b/src/web/WebRenderer.h
@@ -71,7 +71,8 @@ public:
 
   void setCookie(const std::string name, const std::string value,
 		 const WDateTime& expires, const std::string domain,
-		 const std::string path, bool secure);
+		 const std::string path, bool secure,
+		 CookieSameSite = CookieSameSite::Lax);
 
   bool preLearning() const { return learning_; }
   void learningIncomplete();
@@ -103,10 +104,12 @@ private:
     std::string domain;
     WDateTime expires;
     bool secure;
+    CookieSameSite sameSite;
 
     CookieValue();
     CookieValue(const std::string& v, const std::string& p,
-		const std::string& d, const WDateTime& e, bool secure);
+		const std::string& d, const WDateTime& e, bool secure,
+		CookieSameSite sameSite);
   };
 
   WebSession& session_;
diff --git a/src/web/WebSession.C b/src/web/WebSession.C
index 84c58474..05b9f351 100644
--- a/src/web/WebSession.C
+++ b/src/web/WebSession.C
@@ -170,7 +170,7 @@ WebSession::WebSession(WebController *controller,
     sessionIdCookie_ = WRandom::generateId();
     sessionIdCookieChanged_ = true;
     renderer().setCookie("Wt" + sessionIdCookie_, "1", Wt::WDateTime(), "", "",
-			 env_->urlScheme() == "https");
+			 env_->urlScheme() == "https", CookieSameSite::Strict);
   }
 }
 
@@ -3064,14 +3064,15 @@ void WebSession::generateNewSessionId()
 
   if (!useUrlRewriting()) {
     std::string cookieName = env_->deploymentPath();
-    renderer().setCookie(cookieName, sessionId_, WDateTime(), "", "", env_->urlScheme() == "https");
+    renderer().setCookie(cookieName, sessionId_, WDateTime(), "", "",
+			 env_->urlScheme() == "https", CookieSameSite::Strict);
   }
 
   if (controller_->configuration().sessionIdCookie()) {
     sessionIdCookie_ = WRandom::generateId();
     sessionIdCookieChanged_ = true;
     renderer().setCookie("Wt" + sessionIdCookie_, "1", WDateTime(), "", "",
-			 env_->urlScheme() == "https");
+			 env_->urlScheme() == "https", CookieSameSite::Strict);
   }
 }
 #endif // WT_TARGET_JAVA
diff --git a/src/web/skeleton/Boot.js b/src/web/skeleton/Boot.js
index 02c1c8be..da8d242b 100644
--- a/src/web/skeleton/Boot.js
+++ b/src/web/skeleton/Boot.js
@@ -190,14 +190,14 @@ inOneSecond.setTime(inOneSecond.getTime() + 1000);
 
 _$_$if_COOKIE_CHECKS_$_();
 // client-side cookie support
-var testcookie='jscookietest=valid';
+var testcookie='jscookietest=valid;SameSite=Lax';
 doc.cookie=testcookie;
 no_replace = no_replace || 
 	  (_$_USE_COOKIES_$_ && doc.cookie.indexOf(testcookie) != -1);
-doc.cookie=testcookie+';expires=Thu, 01 Jan 1970 00:00:00 GMT';
+doc.cookie=testcookie+';expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Lax';
 
 // server-side cookie support
-doc.cookie='WtTestCookie=ok;path=/;expires=' + inOneSecond.toGMTString();
+doc.cookie='WtTestCookie=ok;path=/;expires=' + inOneSecond.toGMTString() + ';SameSite=Lax';
 _$_$endif_$_();
 
 // hash to query
@@ -287,7 +287,7 @@ if (needSessionInUrl) {
   if (!htmlHistory && canonicalUrl.length > 1) {
 _$_$if_HYBRID_$_();
     var pathcookie='WtInternalPath=' + escape(_$_INTERNAL_PATH_$_)
-      + ';path=/;expires=' + inOneSecond.toGMTString();
+      + ';path=/;expires=' + inOneSecond.toGMTString() + ';SameSite=Lax';
     doc.cookie=pathcookie;
 _$_$endif_$_();
     /* Otherwise we do not get a page reload */
-- 
2.25.4

