From f0282d34343ac666709ef645931e129a4791421a Mon Sep 17 00:00:00 2001 From: TheSaminator Date: Mon, 14 Feb 2022 17:52:48 -0500 Subject: [PATCH] Implement CSRF tokens --- .../kotlin/starshipfights/auth/providers.kt | 16 +++++++--- .../kotlin/starshipfights/auth/utils.kt | 32 +++++++++++++++++++ .../kotlin/starshipfights/info/html_utils.kt | 10 ++++++ .../kotlin/starshipfights/info/views_user.kt | 27 ++++++++++++---- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/jvmMain/kotlin/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/starshipfights/auth/providers.kt index eac6237..a67abf3 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/providers.kt @@ -85,8 +85,8 @@ interface AuthProvider { } post("/me/manage") { + val form = call.receiveValidatedParameters() val currentUser = call.getUser() ?: redirect("/login") - val form = call.receiveParameters() val newUser = currentUser.copy( showDiscordName = form["showdiscord"] == "yes", @@ -117,8 +117,8 @@ interface AuthProvider { } post("/admiral/new") { + val form = call.receiveValidatedParameters() val currentUser = call.getUserSession()?.user ?: redirect("/login") - val form = call.receiveParameters() val newAdmiral = Admiral( owningUser = currentUser, @@ -149,13 +149,14 @@ interface AuthProvider { } post("/admiral/{id}/manage") { + val form = call.receiveValidatedParameters() + val currentUser = call.getUserSession()?.user val admiralId = call.parameters["id"]?.let { Id(it) }!! val admiral = Admiral.get(admiralId)!! if (admiral.owningUser != currentUser) throw ForbiddenException() - val form = call.receiveParameters() val newAdmiral = admiral.copy( name = form["name"]?.takeIf { it.isNotBlank() } ?: admiral.name, isFemale = form["sex"] == "female" @@ -170,6 +171,7 @@ interface AuthProvider { } post("/admiral/{id}/rename/{ship}") { + val formParams = call.receiveValidatedParameters() val currentUser = call.getUserSession()?.user val admiralId = call.parameters["id"]?.let { Id(it) }!! @@ -184,7 +186,7 @@ interface AuthProvider { if (admiral.owningUser != currentUser) throw ForbiddenException() if (ship.owningAdmiral != admiralId) throw ForbiddenException() - val newName = call.receiveParameters()["name"]?.takeIf { it.isNotBlank() && it.length <= SHIP_NAME_MAX_LENGTH } ?: redirect("/admiral/${admiralId}/manage") + val newName = formParams["name"]?.takeIf { it.isNotBlank() && it.length <= SHIP_NAME_MAX_LENGTH } ?: redirect("/admiral/${admiralId}/manage") ShipInDrydock.set(shipId, setValue(ShipInDrydock::name, newName)) redirect("/admiral/${admiralId}/manage") } @@ -194,6 +196,8 @@ interface AuthProvider { } post("/admiral/{id}/sell/{ship}") { + call.receiveValidatedParameters() + val currentUser = call.getUserSession()?.user val admiralId = call.parameters["id"]?.let { Id(it) }!! @@ -224,6 +228,8 @@ interface AuthProvider { } post("/admiral/{id}/buy/{ship}") { + call.receiveValidatedParameters() + val currentUser = call.getUserSession()?.user val admiralId = call.parameters["id"]?.let { Id(it) }!! val admiral = Admiral.get(admiralId)!! @@ -269,6 +275,8 @@ interface AuthProvider { } post("/admiral/{id}/delete") { + call.receiveValidatedParameters() + val currentUser = call.getUserSession()?.user val admiralId = call.parameters["id"]?.let { Id(it) }!! val admiral = Admiral.get(admiralId)!! diff --git a/src/jvmMain/kotlin/starshipfights/auth/utils.kt b/src/jvmMain/kotlin/starshipfights/auth/utils.kt index b5e5669..781acf0 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/utils.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/utils.kt @@ -2,11 +2,16 @@ package starshipfights.auth import io.ktor.application.* import io.ktor.features.* +import io.ktor.http.* import io.ktor.request.* import io.ktor.sessions.* +import io.ktor.util.* +import starshipfights.ForbiddenException import starshipfights.data.Id import starshipfights.data.auth.User import starshipfights.data.auth.UserSession +import starshipfights.data.createNonce +import starshipfights.redirect import java.time.Instant import java.time.temporal.ChronoUnit @@ -41,3 +46,30 @@ object UserSessionIdSerializer : SessionSerializer> { return Id(text) } } + +data class CsrfInput(val cookie: Id, val target: String) + +object CsrfProtector { + private val nonces = mutableMapOf() + + const val csrfInputName = "csrf-token" + + fun newNonce(token: Id, action: String): String { + return createNonce().also { nonces[it] = CsrfInput(token, action) } + } + + fun verifyNonce(nonce: String, token: Id, action: String): Boolean { + return nonces.remove(nonce) == CsrfInput(token, action) + } +} + +suspend fun ApplicationCall.receiveValidatedParameters(): Parameters { + val formInput = receiveParameters() + val sessionId = sessions.get>() ?: redirect("/login") + val csrfToken = formInput.getOrFail(CsrfProtector.csrfInputName) + + if (CsrfProtector.verifyNonce(csrfToken, sessionId, request.uri)) + return formInput + else + throw ForbiddenException() +} diff --git a/src/jvmMain/kotlin/starshipfights/info/html_utils.kt b/src/jvmMain/kotlin/starshipfights/info/html_utils.kt index aefdd94..180d508 100644 --- a/src/jvmMain/kotlin/starshipfights/info/html_utils.kt +++ b/src/jvmMain/kotlin/starshipfights/info/html_utils.kt @@ -1,6 +1,11 @@ package starshipfights.info import kotlinx.html.A +import kotlinx.html.FORM +import kotlinx.html.hiddenInput +import starshipfights.auth.CsrfProtector +import starshipfights.data.Id +import starshipfights.data.auth.UserSession var A.method: String? get() = attributes["data-method"] @@ -10,3 +15,8 @@ var A.method: String? else attributes.remove("data-method") } + +fun FORM.csrfToken(cookie: Id) = hiddenInput { + name = CsrfProtector.csrfInputName + value = CsrfProtector.newNonce(cookie, formAction) +} diff --git a/src/jvmMain/kotlin/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/starshipfights/info/views_user.kt index 2e7cf08..470ca79 100644 --- a/src/jvmMain/kotlin/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/starshipfights/info/views_user.kt @@ -131,6 +131,7 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit { section { h1 { +"User Preferences" } form(method = FormMethod.post, action = "/me/manage") { + csrfToken(currentSession.id) h2 { +"Profile" } @@ -296,7 +297,7 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit { } suspend fun ApplicationCall.createAdmiralPage(): HTML.() -> Unit { - getUser() ?: redirect("/login") + val sessionId = getUserSession()?.id ?: redirect("/login") return page( "Creating Admiral", standardNavBar(), null @@ -304,6 +305,7 @@ suspend fun ApplicationCall.createAdmiralPage(): HTML.() -> Unit { section { h1 { +"Creating Admiral" } form(method = FormMethod.post, action = "/admiral/new") { + csrfToken(sessionId) h3 { label { htmlFor = "faction" @@ -535,7 +537,9 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit { } suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit { - val currentUser = getUserSession()?.user + val currentSession = getUserSession() ?: redirect("/login") + val currentUser = currentSession.user + val admiralId = parameters["id"]?.let { Id(it) }!! val admiral = Admiral.get(admiralId)!! @@ -557,6 +561,7 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit { section { h1 { +"Managing ${admiral.name}" } form(method = FormMethod.post, action = "/admiral/${admiral.id}/manage") { + csrfToken(currentSession.id) h3 { label { htmlFor = "name" @@ -695,7 +700,8 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit { } suspend fun ApplicationCall.renameShipPage(): HTML.() -> Unit { - val currentUser = getUserSession()?.user + val currentSession = getUserSession() ?: redirect("/login") + val currentUser = currentSession.user val admiralId = parameters["id"]?.let { Id(it) }!! val shipId = parameters["ship"]?.let { Id(it) }!! @@ -716,6 +722,7 @@ suspend fun ApplicationCall.renameShipPage(): HTML.() -> Unit { +"${admiral.fullName} is about to rename the ${ship.shipType.fullDisplayName} ${ship.shipData.fullName}. Choose a name here:" } form(method = FormMethod.post, action = "/admiral/${admiral.id}/rename/${ship.id}") { + csrfToken(currentSession.id) textInput(name = "name") { id = "name" value = ship.name @@ -743,7 +750,8 @@ suspend fun ApplicationCall.renameShipPage(): HTML.() -> Unit { } suspend fun ApplicationCall.sellShipConfirmPage(): HTML.() -> Unit { - val currentUser = getUserSession()?.user + val currentSession = getUserSession() ?: redirect("/login") + val currentUser = currentSession.user val admiralId = parameters["id"]?.let { Id(it) }!! val shipId = parameters["ship"]?.let { Id(it) }!! @@ -774,6 +782,7 @@ suspend fun ApplicationCall.sellShipConfirmPage(): HTML.() -> Unit { } } form(method = FormMethod.post, action = "/admiral/${admiral.id}/sell/${ship.id}") { + csrfToken(currentSession.id) submitInput { value = "Sell" } @@ -783,7 +792,9 @@ suspend fun ApplicationCall.sellShipConfirmPage(): HTML.() -> Unit { } suspend fun ApplicationCall.buyShipConfirmPage(): HTML.() -> Unit { - val currentUser = getUserSession()?.user + val currentSession = getUserSession() ?: redirect("/login") + val currentUser = currentSession.user + val admiralId = parameters["id"]?.let { Id(it) }!! val admiral = Admiral.get(admiralId)!! @@ -826,6 +837,7 @@ suspend fun ApplicationCall.buyShipConfirmPage(): HTML.() -> Unit { } } form(method = FormMethod.post, action = "/admiral/${admiral.id}/buy/${shipType.toUrlSlug()}") { + csrfToken(currentSession.id) submitInput { value = "Checkout" } @@ -835,7 +847,9 @@ suspend fun ApplicationCall.buyShipConfirmPage(): HTML.() -> Unit { } suspend fun ApplicationCall.deleteAdmiralConfirmPage(): HTML.() -> Unit { - val currentUser = getUserSession()?.user + val currentSession = getUserSession() ?: redirect("/login") + val currentUser = currentSession.user + val admiralId = parameters["id"]?.let { Id(it) }!! val admiral = Admiral.get(admiralId)!! @@ -857,6 +871,7 @@ suspend fun ApplicationCall.deleteAdmiralConfirmPage(): HTML.() -> Unit { } } form(method = FormMethod.post, action = "/admiral/${admiral.id}/delete") { + csrfToken(currentSession.id) submitInput(classes = "evil") { value = "Yes" } -- 2.25.1