Implement CSRF tokens
authorTheSaminator <TheSaminator@users.noreply.github.com>
Mon, 14 Feb 2022 22:52:48 +0000 (17:52 -0500)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Mon, 14 Feb 2022 22:52:48 +0000 (17:52 -0500)
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/auth/utils.kt
src/jvmMain/kotlin/starshipfights/info/html_utils.kt
src/jvmMain/kotlin/starshipfights/info/views_user.kt

index eac623730a1999e2c4dff232a3b17b5762b8368c..a67abf3d5d6ede222ecb3accd0f1b2c4b0d3e8e1 100644 (file)
@@ -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<Admiral>(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<Admiral>(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<Admiral>(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<Admiral>(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<Admiral>(it) }!!
                                        val admiral = Admiral.get(admiralId)!!
index b5e5669e5e7582463fc2da5c9d3edc70ac42bb59..781acf04f9b7d0f6fa9da690044d9f6b6ce03e4c 100644 (file)
@@ -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<Id<UserSession>> {
                return Id(text)
        }
 }
+
+data class CsrfInput(val cookie: Id<UserSession>, val target: String)
+
+object CsrfProtector {
+       private val nonces = mutableMapOf<String, CsrfInput>()
+       
+       const val csrfInputName = "csrf-token"
+       
+       fun newNonce(token: Id<UserSession>, action: String): String {
+               return createNonce().also { nonces[it] = CsrfInput(token, action) }
+       }
+       
+       fun verifyNonce(nonce: String, token: Id<UserSession>, action: String): Boolean {
+               return nonces.remove(nonce) == CsrfInput(token, action)
+       }
+}
+
+suspend fun ApplicationCall.receiveValidatedParameters(): Parameters {
+       val formInput = receiveParameters()
+       val sessionId = sessions.get<Id<UserSession>>() ?: redirect("/login")
+       val csrfToken = formInput.getOrFail(CsrfProtector.csrfInputName)
+       
+       if (CsrfProtector.verifyNonce(csrfToken, sessionId, request.uri))
+               return formInput
+       else
+               throw ForbiddenException()
+}
index aefdd945aa23a50fabf04986801c9f3b79b689af..180d50823fb949c083523ac8c9870f0e5567f29d 100644 (file)
@@ -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<UserSession>) = hiddenInput {
+       name = CsrfProtector.csrfInputName
+       value = CsrfProtector.newNonce(cookie, formAction)
+}
index 2e7cf08b9f8100dd1914aa6187fb797cd287a82b..470ca795ed8326d7aa5391a79f10140d7da68cf9 100644 (file)
@@ -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<Admiral>(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<Admiral>(it) }!!
        val shipId = parameters["ship"]?.let { Id<ShipInDrydock>(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<Admiral>(it) }!!
        val shipId = parameters["ship"]?.let { Id<ShipInDrydock>(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<Admiral>(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<Admiral>(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"
                                }