Added token based mitigation of CSRF

main
Drew Bednar 2 months ago
parent 12865ca5dd
commit 00ec29d696

@ -263,4 +263,20 @@ It has:
## Local TLS Certs
https://github.com/FiloSottile/mkcert Can be used to create local certs with a trusted CA installed on the machine.
https://github.com/FiloSottile/mkcert Can be used to create local certs with a trusted CA installed on the machine.
## CSRF
- http://www.gnucitizen.org/blog/csrf-demystified/
- https://stackoverflow.com/questions/6412813/do-login-forms-need-tokens-against-csrf-attacks
### Mitigations
- **SameSite cookies**. In chapter 11 we had `Lax`. This means that the session cookie "won't be sent by the browser for POST, PUT, or DELETE requests. As long as you stick to only using POST, PUT, or DELETE for state changing requests (login, signup, create snippet) `Lax` will prevent CRSF attacks.
- **Token-based mitigation** The [OWASP guidance](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#token-based-mitigation)is to use some form of token check if using TLS 1.2 or lower. Like session and password management you may be best served by using a known and tested third party package. This project uses `justinas/nosurf (`gorilla/csrf` is a valid alternative). Both use the [double submit cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) pattern. In this pattern a random CSRF token is generated and sent to the user in a CSRF cookie. This CSRF token is then added to a hidden field in each HTML form that is potentially vulnerable to CSRF. When the form is submitted, both packages use some middleware to check that the hidden field value and cookie value match.
Due to the fact that no browser exists which supports TLS 1.3 and does not support SameSite cookies. If you only allow HTTPS requests to your application and enforce TLS 1.3 as the minimum TLS version, you dont need to make any additional mitigation against CSRF attacks (like using the justinas/nosurf package). Just make sure that you always:
Set SameSite=Lax or SameSite=Strict on the session cookie; and
Use the POST, PUT or DELETE HTTP methods for any state-changing requests.

@ -56,8 +56,12 @@ func main() {
// SessionManager
sm := scs.New()
sm.Store = sqlite3store.New(db)
// ratchet.Version = strings.TrimPrefix(version, "")
// ratchet.Commit = commit
// If you want to change the same sight cookie setting from Lax to Strict
// will block the session cookie being sent by the users browser for all cross-site usage
// including GET and HEAD. That means the cookie won't be sent when clicking on a link in another site
// for a GET request, so the user won't be treated as being "logged in" to the app even if they
// did in another tab
// sm.Cookie.SameSite = http.SameSiteStrictMode
app := server.NewRatchetApp(logger, tc, db, sm)
// these two elliptic curves have assembly implementations

@ -8,5 +8,6 @@ require (
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 // indirect
github.com/alexedwards/scs/v2 v2.8.0 // indirect
github.com/go-playground/form/v4 v4.2.1 // indirect
github.com/justinas/nosurf v1.1.1 // indirect
golang.org/x/crypto v0.32.0 // indirect
)

@ -5,6 +5,8 @@ github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gv
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

@ -6,6 +6,7 @@ import (
"net/http"
"github.com/alexedwards/scs/v2"
"github.com/justinas/nosurf"
)
// https://owasp.org/www-project-secure-headers/ guidance
@ -122,3 +123,16 @@ func RequireAuthenticationMiddleware(next http.Handler, sm *scs.SessionManager)
next.ServeHTTP(w, r)
})
}
// NoSurfMiddleware uses the noSurf package to create a customized CSRF cookie
// with the Secure, Path and HttpOnly attributes set
func NoSurfMiddleware(next http.Handler) http.Handler {
csrfHandler := nosurf.New(next)
csrfHandler.SetBaseCookie(http.Cookie{
HttpOnly: true,
Path: "/",
Secure: true,
})
return csrfHandler
}

@ -23,21 +23,21 @@ func addRoutes(mux *http.ServeMux,
// resulting in this route requiring an exact match on "/" only
// You can only include one HTTP method in a route pattern if you choose
// GET will match GET & HEAD http request methods
mux.Handle("GET /{$}", sm.LoadAndSave(handleHome(logger, tc, sm, snippetService))) // might be time to swith to github.com/justinas/alice dynamic chain
mux.Handle("GET /snippet/view/{id}", sm.LoadAndSave(handleSnippetView(logger, tc, sm, snippetService)))
mux.Handle("GET /snippet/create", sm.LoadAndSave(RequireAuthenticationMiddleware(handleSnippetCreateGet(tc, sm), sm)))
mux.Handle("POST /snippet/create", sm.LoadAndSave(RequireAuthenticationMiddleware(handleSnippetCreatePost(logger, tc, fd, sm, snippetService), sm)))
mux.Handle("GET /{$}", sm.LoadAndSave(NoSurfMiddleware(handleHome(logger, tc, sm, snippetService)))) // might be time to swith to github.com/justinas/alice dynamic chain
mux.Handle("GET /snippet/view/{id}", sm.LoadAndSave(NoSurfMiddleware(handleSnippetView(logger, tc, sm, snippetService))))
mux.Handle("GET /snippet/create", sm.LoadAndSave(NoSurfMiddleware(RequireAuthenticationMiddleware(handleSnippetCreateGet(tc, sm), sm))))
mux.Handle("POST /snippet/create", sm.LoadAndSave(NoSurfMiddleware(RequireAuthenticationMiddleware(handleSnippetCreatePost(logger, tc, fd, sm, snippetService), sm))))
// mux.Handle("/something", handleSomething(logger, config))
// mux.Handle("/healthz", handleHealthzPlease(logger))
// mux.Handle("/", http.NotFoundHandler())
mux.Handle("GET /user/signup", sm.LoadAndSave(handleUserSignupGet(tc, sm)))
mux.Handle("POST /user/signup", sm.LoadAndSave(handleUserSignupPost(logger, tc, fd, sm, userService)))
mux.Handle("GET /user/login", sm.LoadAndSave(handleUserLoginGet(tc, sm)))
mux.Handle("POST /user/login", sm.LoadAndSave(handleUserLoginPost(logger, tc, sm, fd, userService)))
mux.Handle("GET /user/signup", sm.LoadAndSave(NoSurfMiddleware(handleUserSignupGet(tc, sm))))
mux.Handle("POST /user/signup", sm.LoadAndSave(NoSurfMiddleware(handleUserSignupPost(logger, tc, fd, sm, userService))))
mux.Handle("GET /user/login", sm.LoadAndSave(NoSurfMiddleware(handleUserLoginGet(tc, sm))))
mux.Handle("POST /user/login", sm.LoadAndSave(NoSurfMiddleware(handleUserLoginPost(logger, tc, sm, fd, userService))))
// Requires auth
mux.Handle("POST /user/logout", sm.LoadAndSave(RequireAuthenticationMiddleware(handleUserLogoutPost(logger, sm), sm)))
mux.Handle("POST /user/logout", sm.LoadAndSave(NoSurfMiddleware(RequireAuthenticationMiddleware(handleUserLogoutPost(logger, sm), sm))))
return mux
}

@ -11,6 +11,7 @@ import (
"git.runcible.io/learning/ratchet/internal/model"
"github.com/alexedwards/scs/v2"
"github.com/justinas/nosurf"
)
// Define a templateData type to act as the holding structure for
@ -24,13 +25,17 @@ type templateData struct {
Form any
Flash string
IsAuthenticated bool
CSRFToken string
}
// newTemplateData is useful to inject default values. Example CSRF tokens for forms.
func newTemplateData(r *http.Request, sm *scs.SessionManager) templateData {
return templateData{CurrentYear: time.Now().Year(),
Flash: sm.PopString(r.Context(), "flash"),
IsAuthenticated: isAuthenticated(r, sm)}
IsAuthenticated: isAuthenticated(r, sm),
// added to every page because the form for logout can appear on every page
CSRFToken: nosurf.Token(r),
}
}
// TEMPLATE FUNCTIONS

@ -2,6 +2,8 @@
{{define "main"}}
<form action='/snippet/create' method='POST'>
<!-- Include the CSRF token -->
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<div>
<label>Title:</label>
<!-- Use the `with` action to render the value of .Form.FieldErrors.title

@ -2,6 +2,8 @@
{{define "main"}}
<form action='/user/login' method='POST' novalidate>
<!-- Include the CSRF token -->
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<!-- Notice that here we are looping over the NonFieldErrors and displaying
them, if any exist -->
{{range .Form.NonFieldErrors}}

@ -2,6 +2,8 @@
{{define "main"}}
<form action='/user/signup' method='POST' novalidate>
<!-- Include the CSRF token -->
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<div>
<label>Name:</label>
{{with .Form.FieldErrors.name}}

@ -9,6 +9,8 @@
<div>
{{ if .IsAuthenticated }}
<form action='/user/logout' method='POST'>
<!-- Include the CSRF token -->
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
<button>Logout</button>
</form>
{{ else }}

Loading…
Cancel
Save