From ca1418fc0135d52a009ab218d6e24187fb355a3c Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 09:09:10 -0800 Subject: security: implement CSRF protection and improve session cookie security (fixing NK-gfh33y) --- web/web.go | 48 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) (limited to 'web/web.go') diff --git a/web/web.go b/web/web.go index da951e5..3c53edf 100644 --- a/web/web.go +++ b/web/web.go @@ -1,7 +1,9 @@ package web import ( + "crypto/rand" "encoding/base64" + "encoding/hex" "fmt" "io/fs" "io/ioutil" @@ -114,7 +116,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") if password == config.Config.DigestPassword { v, _ := bcrypt.GenerateFromPassword([]byte(password), 0) - c := http.Cookie{Name: AuthCookie, Value: string(v), Path: "/", MaxAge: SecondsInAYear, HttpOnly: false} + c := http.Cookie{Name: AuthCookie, Value: string(v), Path: "/", MaxAge: SecondsInAYear, HttpOnly: true} http.SetCookie(w, &c) http.Redirect(w, r, "/", 307) } else { @@ -126,7 +128,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { } func logoutHandler(w http.ResponseWriter, r *http.Request) { - c := http.Cookie{Name: AuthCookie, MaxAge: 0, Path: "/", HttpOnly: false} + c := http.Cookie{Name: AuthCookie, MaxAge: 0, Path: "/", HttpOnly: true} http.SetCookie(w, &c) fmt.Fprintf(w, "you are logged out") } @@ -195,7 +197,7 @@ func apiLoginHandler(w http.ResponseWriter, r *http.Request) { if password == config.Config.DigestPassword { v, _ := bcrypt.GenerateFromPassword([]byte(password), 0) - c := http.Cookie{Name: AuthCookie, Value: string(v), Path: "/", MaxAge: SecondsInAYear, HttpOnly: false} + c := http.Cookie{Name: AuthCookie, Value: string(v), Path: "/", MaxAge: SecondsInAYear, HttpOnly: true} http.SetCookie(w, &c) w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok"}`) @@ -258,7 +260,7 @@ func NewRouter(cfg *config.Settings) http.Handler { mux.Handle("/", GzipMiddleware(AuthWrap(http.HandlerFunc(indexHandler)))) - return mux + return CSRFMiddleware(mux) } func Serve(cfg *config.Settings) { @@ -341,8 +343,44 @@ func GzipMiddleware(next http.Handler) http.Handler { } func apiLogoutHandler(w http.ResponseWriter, r *http.Request) { - c := http.Cookie{Name: AuthCookie, Value: "", Path: "/", MaxAge: -1, HttpOnly: false} + c := http.Cookie{Name: AuthCookie, Value: "", Path: "/", MaxAge: -1, HttpOnly: true} http.SetCookie(w, &c) w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok"}`) } + +func generateRandomToken(n int) string { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "" + } + return hex.EncodeToString(b) +} + +func CSRFMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("csrf_token") + var token string + if err != nil { + token = generateRandomToken(16) + http.SetCookie(w, &http.Cookie{ + Name: "csrf_token", + Value: token, + Path: "/", + HttpOnly: false, // accessible by JS + SameSite: http.SameSiteLaxMode, + }) + } else { + token = cookie.Value + } + + if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete { + headerToken := r.Header.Get("X-CSRF-Token") + if headerToken == "" || headerToken != token { + http.Error(w, "CSRF token mismatch", http.StatusForbidden) + return + } + } + next.ServeHTTP(w, r) + }) +} -- cgit v1.2.3