diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-14 09:09:10 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-14 09:09:10 -0800 |
| commit | ca1418fc0135d52a009ab218d6e24187fb355a3c (patch) | |
| tree | 95f54977609ec401f8439a30e3a158c36a5526bf /web/web.go | |
| parent | a39dfd30529330e3eea44bce865093158eaf2f1b (diff) | |
| download | neko-ca1418fc0135d52a009ab218d6e24187fb355a3c.tar.gz neko-ca1418fc0135d52a009ab218d6e24187fb355a3c.tar.bz2 neko-ca1418fc0135d52a009ab218d6e24187fb355a3c.zip | |
security: implement CSRF protection and improve session cookie security (fixing NK-gfh33y)
Diffstat (limited to 'web/web.go')
| -rw-r--r-- | web/web.go | 48 |
1 files changed, 43 insertions, 5 deletions
@@ -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) + }) +} |
