feat: CSRF protection on the login and URL-generator forms (closes #93)
check / check (push) Failing after 0s
check / check (push) Failing after 0s
Both cookie-authenticated HTML form posts (POST / and POST /generate) now require a CSRF token via gorilla/csrf, the recorded default in GO_PACKAGE_DEFAULTS.md. The token cookie is independent of the session cookie, so it also covers the login POST, where no session exists yet (login CSRF). The token key is derived from the signing key with its own HKDF salt, so tokens survive restarts and reuse no other key material; gorilla/csrf supplies crypto/rand generation and constant-time compare. The form routes sit in a chi group behind the middleware; the hidden token field is rendered into login.html and generator.html. In local plaintext HTTP mode (debug) requests are marked plaintext so the library does not demand an https Referer or set a Secure cookie the browser would withhold; in production, behind the TLS-terminating proxy, it enforces its https Referer origin check. model: claude-opus-4-8
This commit is contained in:
+25
-15
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -23,13 +24,13 @@ func (s *Handlers) HandleRoot() http.HandlerFunc {
|
||||
|
||||
// Check if authenticated
|
||||
if s.sessMgr.IsAuthenticated(r) {
|
||||
s.renderGenerator(w, nil)
|
||||
s.renderGenerator(w, r, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Show login page
|
||||
s.renderLogin(w, "")
|
||||
s.renderLogin(w, r, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ func (s *Handlers) HandleRoot() http.HandlerFunc {
|
||||
func (s *Handlers) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderLogin(w, "Invalid form data")
|
||||
s.renderLogin(w, r, "Invalid form data")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -47,7 +48,7 @@ func (s *Handlers) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare([]byte(submittedKey), []byte(s.config.SigningKey)) != 1 {
|
||||
s.log.Warn("failed login attempt", "remote_addr", r.RemoteAddr)
|
||||
s.renderLogin(w, "Invalid signing key")
|
||||
s.renderLogin(w, r, "Invalid signing key")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -56,7 +57,7 @@ func (s *Handlers) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
err = s.sessMgr.CreateSession(w)
|
||||
if err != nil {
|
||||
s.log.Error("failed to create session", "error", err)
|
||||
s.renderLogin(w, "Failed to create session")
|
||||
s.renderLogin(w, r, "Failed to create session")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -87,7 +88,7 @@ func (s *Handlers) HandleGenerateURL() http.HandlerFunc {
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
s.renderGenerator(w, &generatorData{Error: "Invalid form data"})
|
||||
s.renderGenerator(w, r, &generatorData{Error: "Invalid form data"})
|
||||
|
||||
return
|
||||
}
|
||||
@@ -97,7 +98,7 @@ func (s *Handlers) HandleGenerateURL() http.HandlerFunc {
|
||||
// Validate source URL
|
||||
parsed, err := url.Parse(sourceURL)
|
||||
if err != nil || parsed.Host == "" {
|
||||
s.renderGeneratorWithForm(w, "Invalid source URL", r.Form)
|
||||
s.renderGeneratorWithForm(w, r, "Invalid source URL", r.Form)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -108,7 +109,7 @@ func (s *Handlers) HandleGenerateURL() http.HandlerFunc {
|
||||
token, err := s.encGen.Generate(payload)
|
||||
if err != nil {
|
||||
s.log.Error("failed to generate encrypted URL", "error", err)
|
||||
s.renderGeneratorWithForm(w, "Failed to generate URL", r.Form)
|
||||
s.renderGeneratorWithForm(w, r, "Failed to generate URL", r.Form)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -121,7 +122,7 @@ func (s *Handlers) HandleGenerateURL() http.HandlerFunc {
|
||||
expiresAtStr = expiresAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
s.renderGenerator(w, &generatorData{
|
||||
s.renderGenerator(w, r, &generatorData{
|
||||
GeneratedURL: generatedURL,
|
||||
ExpiresAt: expiresAtStr,
|
||||
FormURL: sourceURL,
|
||||
@@ -186,15 +187,20 @@ type generatorData struct {
|
||||
FormQuality string
|
||||
FormFit string
|
||||
FormTTL string
|
||||
CSRFField template.HTML
|
||||
}
|
||||
|
||||
func (s *Handlers) renderLogin(w http.ResponseWriter, errorMsg string) {
|
||||
func (s *Handlers) renderLogin(
|
||||
w http.ResponseWriter, r *http.Request, errorMsg string,
|
||||
) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
data := struct {
|
||||
Error string
|
||||
Error string
|
||||
CSRFField template.HTML
|
||||
}{
|
||||
Error: errorMsg,
|
||||
Error: errorMsg,
|
||||
CSRFField: csrfField(r),
|
||||
}
|
||||
|
||||
err := templates.Render(w, "login.html", data)
|
||||
@@ -204,13 +210,17 @@ func (s *Handlers) renderLogin(w http.ResponseWriter, errorMsg string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Handlers) renderGenerator(w http.ResponseWriter, data *generatorData) {
|
||||
func (s *Handlers) renderGenerator(
|
||||
w http.ResponseWriter, r *http.Request, data *generatorData,
|
||||
) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
if data == nil {
|
||||
data = &generatorData{}
|
||||
}
|
||||
|
||||
data.CSRFField = csrfField(r)
|
||||
|
||||
err := templates.Render(w, "generator.html", data)
|
||||
if err != nil {
|
||||
s.log.Error("failed to render generator template", "error", err)
|
||||
@@ -219,9 +229,9 @@ func (s *Handlers) renderGenerator(w http.ResponseWriter, data *generatorData) {
|
||||
}
|
||||
|
||||
func (s *Handlers) renderGeneratorWithForm(
|
||||
w http.ResponseWriter, errorMsg string, form url.Values,
|
||||
w http.ResponseWriter, r *http.Request, errorMsg string, form url.Values,
|
||||
) {
|
||||
s.renderGenerator(w, &generatorData{
|
||||
s.renderGenerator(w, r, &generatorData{
|
||||
Error: errorMsg,
|
||||
FormURL: form.Get("url"),
|
||||
FormWidth: form.Get("width"),
|
||||
|
||||
Reference in New Issue
Block a user