diff --git a/app/controllers/LoginController.scala b/app/controllers/LoginController.scala index 503133c24..5b7605c9e 100644 --- a/app/controllers/LoginController.scala +++ b/app/controllers/LoginController.scala @@ -6,14 +6,13 @@ import helper.{StringHelper, Time} import java.time.ZoneId import javax.inject.{Inject, Singleton} import models.EventType.{GenerateToken, UnknownEmail} -import models.{Authorization, LoginToken, User} +import models.{Authorization, EventType, LoginToken, User} import org.webjars.play.WebJarsUtil import play.api.Configuration import play.api.mvc.{Action, AnyContent, InjectedController, Request} import serializers.Keys import services.{EventService, NotificationService, SignupService, TokenService, UserService} import views.home.LoginPanel - import scala.concurrent.{ExecutionContext, Future} @Singleton @@ -159,7 +158,16 @@ class LoginController @Inject() ( request.getQueryString(Keys.QueryParam.token), request.getQueryString(Keys.QueryParam.path) ) match { - case (Some(token), Some(path)) => + case (Some(token), Some(uncheckedPath)) => + val path = + if (PathValidator.isValidPath(uncheckedPath)) uncheckedPath + else { + eventService.logSystem( + EventType.LoginInvalidPath, + s"Redirection invalide après le login '$uncheckedPath'" + ) + routes.HomeController.index.url + } Ok( views.html.magicLinkAntiConsumptionPage( token = token, diff --git a/app/controllers/PathValidator.scala b/app/controllers/PathValidator.scala new file mode 100644 index 000000000..07173dec0 --- /dev/null +++ b/app/controllers/PathValidator.scala @@ -0,0 +1,56 @@ +package controllers + +import java.util.UUID +import models.EventType +import play.api.mvc.Call +import scala.util.Try +import scala.util.matching.Regex + +object PathValidator { + + // We put in the whitelist paths used in emails and + // paths that might be used as bookmarks. + // + // Note that we cannot use Play's router to validate an url, hence the regexes. + private val pathWhitelist: List[Regex] = { + val placeholder = "00000000-0000-0000-0000-000000000000" + val placeholderUUID = UUID.fromString(placeholder) + val calls: List[Call] = List( + routes.HomeController.index, + routes.HomeController.help, + routes.HomeController.welcome, + routes.ApplicationController.create, + routes.ApplicationController.myApplications, + routes.ApplicationController.show(placeholderUUID), + routes.MandatController.mandat(placeholderUUID), + routes.ApplicationController.stats, + routes.UserController.showEditProfile, + routes.UserController.home, + routes.UserController.editUser(placeholderUUID), + routes.GroupController.showEditMyGroups, + routes.GroupController.editGroup(placeholderUUID), + routes.UserController.add(placeholderUUID), + routes.UserController.showValidateAccount, + routes.AreaController.all, + routes.AreaController.deploymentDashboard, + routes.AreaController.franceServiceDeploymentDashboard, + routes.ApplicationController.all(placeholderUUID), + routes.UserController.all(placeholderUUID), + ) + val uuidRegex = "([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})" + calls.map(call => + // this compiles the regex + new Regex("^" + call.path().replace(placeholder, uuidRegex) + "$") + ) + } + + def isValidPath(path: String): Boolean = + pathWhitelist.exists { pathRegex => + path match { + case pathRegex(uuids @ _*) => + uuids.forall(uuid => Try(UUID.fromString(uuid)).isSuccess) + case _ => false + } + } + +} diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 1b5901e0e..eccab5be1 100644 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -737,6 +737,17 @@ case class UserController @Inject() ( def validateAccount: Action[AnyContent] = loginAction.async { implicit request => val user = request.currentUser + + def validateRedirect(uncheckedRedirect: String): String = + if (PathValidator.isValidPath(uncheckedRedirect)) uncheckedRedirect + else { + eventService.log( + EventType.CGUInvalidRedirect, + s"URL de redirection après les CGU invalide '$uncheckedRedirect'" + ) + routes.HomeController.index.url + } + ValidateSubscriptionForm .validate(user) .bindFromRequest() @@ -747,13 +758,14 @@ case class UserController @Inject() ( }, { case ValidateSubscriptionForm( - Some(redirect), + Some(uncheckedRedirect), true, firstName, lastName, qualite, phoneNumber - ) if redirect =!= routes.ApplicationController.myApplications.url => + ) if uncheckedRedirect =!= routes.ApplicationController.myApplications.url => + val redirect = validateRedirect(uncheckedRedirect) validateAndUpdateUser(request.currentUser)(firstName, lastName, qualite, phoneNumber) .map { updatedUser => val logMessage = @@ -771,8 +783,9 @@ case class UserController @Inject() ( Redirect(routes.HomeController.welcome) .flashing("success" -> "Merci d’avoir accepté les CGU") } - case ValidateSubscriptionForm(Some(redirect), false, _, _, _, _) - if redirect =!= routes.ApplicationController.myApplications.url => + case ValidateSubscriptionForm(Some(uncheckedRedirect), false, _, _, _, _) + if uncheckedRedirect =!= routes.ApplicationController.myApplications.url => + val redirect = validateRedirect(uncheckedRedirect) Future(Redirect(Call("GET", redirect))) case ValidateSubscriptionForm(_, false, _, _, _, _) => Future(Redirect(routes.HomeController.welcome)) diff --git a/app/models/EventType.scala b/app/models/EventType.scala index 22565e5bb..1a4658b5e 100644 --- a/app/models/EventType.scala +++ b/app/models/EventType.scala @@ -66,6 +66,7 @@ object EventType { object CGUShowed extends Info object CGUValidated extends Info object CGUValidationError extends Error + object CGUInvalidRedirect extends Warn object ChangeAreaUnauthorized extends Warn object DeleteUserUnauthorized extends Warn object DeploymentDashboardUnauthorized extends Warn @@ -142,6 +143,7 @@ object EventType { object StatsIncorrectSetup extends Warn object TryLoginByKey extends Info object UnknownEmail extends Warn + object LoginInvalidPath extends Warn object UserProfileShowed extends Info object UserProfileShowedError extends Error