~jan0sch/smederee
Showing details for patch 66a155b733f75ab7ecff13a8db6ffbf39b731f9d.
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)