CVE-2020-7780: Cross-Site Request Forgery (CSRF) (CWE-352)
value: This affects the package com.softwaremill.akka-http-session:core_2.13 before 0.5.11; the package com.softwaremill.akka-http-session:core_2.12 before 0.5.11; the package com.softwaremill.akka-http-session:core_2.11 before 0.5.11. For older versions, endpoints protected by randomTokenCsrfProtection could be bypassed with an empty X-XSRF-TOKEN header and an empty XSRF-TOKEN cookie.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2020-11-27T17:15Z
Repository: https://github.com/softwaremill/akka-http-session
Language: Scala
Files changed: core/src/main/scala/com/softwaremill/session/CsrfDirectives.scala, core/src/test/scala/com/softwaremill/session/CsrfDirectivesTest.scala, javaTests/src/test/java/com/softwaremill/session/javadsl/CsrfDirectivesTest.java
Commit date: 2020-03-11 18:09:06 +0100
Commit message: Fix CSRF protection bypass #74

Diff: diff --git a/core/src/main/scala/com/softwaremill/session/CsrfDirectives.scala b/core/src/main/scala/com/softwaremill/session/CsrfDirectives.scala
index cfe21c1..f6d8f30 100644
--- a/core/src/main/scala/com/softwaremill/session/CsrfDirectives.scala
+++ b/core/src/main/scala/com/softwaremill/session/CsrfDirectives.scala
@@ -21,11 +21,11 @@ trait CsrfDirectives {
       case Some(cookie) =>
         // if a cookie is already set, we let through all get requests (without setting a new token), or validate
         // that the token matches.
         get.recover { _ =>
           submittedCsrfToken(checkMode).flatMap { submitted =>
-            if (submitted == cookie) {
+            if (submitted == cookie && !cookie.isEmpty) {
               pass
             } else {
               reject(checkMode.csrfManager.tokenInvalidRejection).toDirective[Unit]
             }
           }
diff --git a/core/src/test/scala/com/softwaremill/session/CsrfDirectivesTest.scala b/core/src/test/scala/com/softwaremill/session/CsrfDirectivesTest.scala
index a02380f..ea6b2a7 100644
--- a/core/src/test/scala/com/softwaremill/session/CsrfDirectivesTest.scala
+++ b/core/src/test/scala/com/softwaremill/session/CsrfDirectivesTest.scala
@@ -79,10 +79,24 @@ class CsrfDirectivesTest extends FlatSpec with ScalatestRouteTest with Matchers
           rejections should be(List(AuthorizationFailedRejection))
         }
     }
   }
 
+  it should "reject requests if the csrf cookie and the header are empty" in {
+    Get("/site") ~> routes ~> check {
+      responseAs[String] should be("ok")
+
+      Post("/transfer_money") ~>
+        addHeader(Cookie(cookieName, "")) ~>
+        addHeader(sessionConfig.csrfSubmittedName, "") ~>
+        routes ~>
+        check {
+          rejections should be(List(AuthorizationFailedRejection))
+        }
+    }
+  }
+
   it should "accept requests if the csrf cookie matches the header value" in {
     Get("/site") ~> routes ~> check {
       responseAs[String] should be("ok")
       val Some(csrfCookie) = header[`Set-Cookie`]
 
diff --git a/javaTests/src/test/java/com/softwaremill/session/javadsl/CsrfDirectivesTest.java b/javaTests/src/test/java/com/softwaremill/session/javadsl/CsrfDirectivesTest.java
index e671861..3ddf781 100644
--- a/javaTests/src/test/java/com/softwaremill/session/javadsl/CsrfDirectivesTest.java
+++ b/javaTests/src/test/java/com/softwaremill/session/javadsl/CsrfDirectivesTest.java
@@ -133,10 +133,37 @@ public class CsrfDirectivesTest extends HttpSessionAwareDirectivesTest {
             .assertStatusCode(StatusCodes.FORBIDDEN);
 
 
     }
 
+    @Test
+    public void shouldRejectRequestsIfTheCsrfCookieAndTheHeaderAreEmpty() {
+        // given
+        final Route route = createCsrfRouteWithCheckHeaderMode();
+
+        // when
+        TestRouteResult testRouteResult = testRoute(route)
+          .run(HttpRequest.GET("/site"));
+
+        // then
+        testRouteResult
+          .assertStatusCode(StatusCodes.OK);
+
+        /* second request */
+        // when
+        TestRouteResult testRouteResult2 = testRoute(route)
+          .run(HttpRequest.POST("/transfer_money")
+            .addHeader(Cookie.create(csrfCookieName, ""))
+            .addHeader(RawHeader.create(csrfSubmittedName, ""))
+          );
+
+        // then
+        testRouteResult2
+          .assertStatusCode(StatusCodes.FORBIDDEN);
+
+    }
+
     @Test
     public void shouldAcceptRequestsIfTheCsrfCookieMatchesTheHeaderValue() {
         // given
         final Route route = createCsrfRouteWithCheckHeaderMode();
 

Vulnerable code:             if (submitted == cookie) {
Fixed code:             if (submitted == cookie && !cookie.isEmpty) {
  it should "reject requests if the csrf cookie and the header are empty" in {
    Get("/site") ~> routes ~> check {
      responseAs[String] should be("ok")

      Post("/transfer_money") ~>
        addHeader(Cookie(cookieName, "")) ~>
        addHeader(sessionConfig.csrfSubmittedName, "") ~>
        routes ~>
        check {
          rejections should be(List(AuthorizationFailedRejection))
        }
    }
  }

    @Test
    public void shouldRejectRequestsIfTheCsrfCookieAndTheHeaderAreEmpty() {
        // given
        final Route route = createCsrfRouteWithCheckHeaderMode();

        // when
        TestRouteResult testRouteResult = testRoute(route)
          .run(HttpRequest.GET("/site"));

        // then
        testRouteResult
          .assertStatusCode(StatusCodes.OK);

        /* second request */
        // when
        TestRouteResult testRouteResult2 = testRoute(route)
          .run(HttpRequest.POST("/transfer_money")
            .addHeader(Cookie.create(csrfCookieName, ""))
            .addHeader(RawHeader.create(csrfSubmittedName, ""))
          );

        // then
        testRouteResult2
          .assertStatusCode(StatusCodes.FORBIDDEN);

    }