~jan0sch/smederee

Showing details for patch 66a155b733f75ab7ecff13a8db6ffbf39b731f9d.
2022-11-04 (Fri), 4:41 PM - Jens Grassel - 66a155b733f75ab7ecff13a8db6ffbf39b731f9d

CSRF: Activate and use the CSRF protection middleware from http4s.

Summary of changes
2 files modified with 44 lines added and 3 lines removed
  • CHANGELOG.md with 4 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/HubServer.scala with 40 added and 3 removed lines
diff -rN -u old-smederee/CHANGELOG.md new-smederee/CHANGELOG.md
--- old-smederee/CHANGELOG.md	2025-02-01 15:43:29.965600526 +0000
+++ new-smederee/CHANGELOG.md	2025-02-01 15:43:29.969600532 +0000
@@ -20,6 +20,10 @@
 
 ## Unreleased
 
+### Added
+
+- use the CSRF protection middleware of http4s
+
 ### Fixed
 
 - history summary shows wrong number of lines that were added/removed
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-01 15:43:29.969600532 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-01 15:43:29.969600532 +0000
@@ -21,12 +21,14 @@
 import java.nio.file._
 import java.nio.file.attribute.PosixFilePermissions
 
+import cats.arrow.FunctionK
 import cats.effect._
 import cats.syntax.all._
 import com.comcast.ip4s._
 import com.typesafe.config._
 import de.smederee.darcs._
 import de.smederee.email.SimpleJavaMailMiddleware
+import de.smederee.html.LinkTools._
 import de.smederee.hub.config._
 import de.smederee.security._
 import de.smederee.ssh._
@@ -36,6 +38,7 @@
 import org.http4s.ember.server._
 import org.http4s.implicits._
 import org.http4s.server._
+import org.http4s.server.middleware.CSRF
 import org.http4s.server.staticcontent.resourceServiceBuilder
 import org.slf4j.LoggerFactory
 import pureconfig._
@@ -47,6 +50,19 @@
 object HubServer extends IOApp with AuthenticationMiddleware {
   private val log = LoggerFactory.getLogger(getClass)
 
+  /** Create a function to perform the origin checks for the CSRF protection middleware using the configuration.
+    *
+    * @param linkConfig
+    *   Settings affecting how the service will communicate several information to the "outside world" e.g. if it runs
+    *   behind a reverse proxy. These settings are needed to set the correct values for expected hostname, port and
+    *   transport scheme of the cookies.
+    * @return
+    *   A function which will check the correct origin of requests / cookies inside the CSRF middleware.
+    */
+  private def createCsrfOriginCheck(linkConfig: ExternalLinkConfig): Request[IO] => Boolean = { request =>
+    CSRF.defaultOriginCheck(request, linkConfig.host.toString, linkConfig.scheme, linkConfig.port.map(_.value))
+  }
+
   def run(args: List[String]): IO[ExitCode] = {
     val databaseMigrator = new DatabaseMigrator[IO]
     for {
@@ -90,7 +106,28 @@
         configuration.database.user,
         configuration.database.pass
       )
-      cryptoClock        = java.time.Clock.systemUTC
+      cryptoClock = java.time.Clock.systemUTC
+      csrfKey <- CSRF.generateSigningKey[IO]()
+      csrfOriginCheck = createCsrfOriginCheck(configuration.service.external)
+      csrfBuilder     = CSRF[IO, IO](csrfKey, csrfOriginCheck)
+      /* The idea behind the `onFailure` part of the CSRF protection middleware is
+       * that we simply remove the CSRF cookie and redirect the user to the frontpage.
+       * This is done to avoid frustration for users after a server restart because
+       * the CSRF secret key will change then and thus all requests are invalid.
+       */
+      csrfMiddleware = csrfBuilder
+        .withClock(cryptoClock)
+        .withCookieDomain(Option(configuration.service.external.host.toString))
+        .withCookieName(Constants.csrfCookieName.toString)
+        .withCookiePath(Option("/"))
+        .withCSRFCheck(CSRF.checkCSRFinHeaderAndForm[IO, IO](Constants.csrfCookieName.toString, FunctionK.id))
+        .withOnFailure(
+          Response[IO](
+            headers = Headers(List(headers.Location(configuration.service.external.createFullUri(uri"/")))),
+            status = Status.SeeOther
+          ).removeCookie(Constants.csrfCookieName.toString)
+        )
+        .build
       signAndValidate    = SignAndValidate(configuration.service.authentication.cookieSecret)
       assetsRoutes       = resourceServiceBuilder[IO](Constants.assetsPath.path.toAbsolute.toString).toRoutes
       authenticationRepo = new DoobieAuthenticationRepository[IO](transactor)
@@ -136,7 +173,7 @@
           vcsRepoRoutes.protectedRoutes <+>
           landingPages.protectedRoutes
       )
-      globalRoutes = Router(
+      hubWebService = Router(
         Constants.assetsPath.path.toAbsolute.toString -> assetsRoutes,
         "/" -> (protectedRoutesWithFallThrough <+>
           authenticationRoutes.routes <+>
@@ -163,7 +200,7 @@
         .default[IO]
         .withHost(configuration.service.host)
         .withPort(configuration.service.port)
-        .withHttpApp(globalRoutes)
+        .withHttpApp(csrfMiddleware.validate()(hubWebService))
         .build
       webServer = resource.use(server =>
         IO(log.info("Server started at {}", server.address)) >> IO.never.as(ExitCode.Success)