diff --git a/altcha.go b/altcha.go new file mode 100644 index 0000000..ea5e50b --- /dev/null +++ b/altcha.go @@ -0,0 +1,27 @@ +package main + +import ( + "net/http" + + goaltcha "github.com/k42-software/go-altcha" + altchahttp "github.com/k42-software/go-altcha/http" +) + +func serveAltchaJS(w http.ResponseWriter, r *http.Request) { + altchahttp.ServeJavascript(w, r) +} + +func serveAltchaChallenge(w http.ResponseWriter, r *http.Request) { + challenge := goaltcha.NewChallenge() + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate") + _, _ = w.Write([]byte(challenge.Encode())) +} + +func validateAltcha(r *http.Request) bool { + encoded := r.PostFormValue("altcha") + if encoded == "" { + return false + } + return goaltcha.ValidateResponse(encoded, true) +} diff --git a/change.go b/change.go index 8d1b32f..0a9e7e6 100644 --- a/change.go +++ b/change.go @@ -53,6 +53,12 @@ func changePassword(w http.ResponseWriter, r *http.Request) { return } + if !validateAltcha(r) { + csrfToken, _ := setCSRFToken(w) + displayTmplError(w, http.StatusForbidden, "change.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again.", "csrf_token": csrfToken}) + return + } + renderError := func(status int, msg string) { csrfToken, _ := setCSRFToken(w) displayTmplError(w, status, "change.html", map[string]interface{}{"error": msg, "csrf_token": csrfToken}) diff --git a/go.mod b/go.mod index 17e467e..eb358eb 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/k42-software/go-altcha v0.1.1 + github.com/pkg/errors v0.9.1 // indirect golang.org/x/crypto v0.36.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/go.sum b/go.sum index ea772ab..318fb83 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/k42-software/go-altcha v0.1.1 h1:vfA+0+0gr7jK4vp21Q7xvEpIjDsx8PqzxS0obgIToQs= +github.com/k42-software/go-altcha v0.1.1/go.mod h1:2aX+0PkUSI0YPDVfjapZeuGELWt8ugEXkg8gr6QejMU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/login.go b/login.go index cd72b3c..44ffe66 100644 --- a/login.go +++ b/login.go @@ -53,6 +53,11 @@ func tryLogin(w http.ResponseWriter, r *http.Request) { return } + if !validateAltcha(r) { + displayTmplError(w, http.StatusForbidden, "login.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again."}) + return + } + if entries, err := login(r.PostFormValue("login"), r.PostFormValue("password")); err != nil { log.Println(err) displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]interface{}{"error": err.Error()}) diff --git a/lost.go b/lost.go index 250ac42..1e4c9d3 100644 --- a/lost.go +++ b/lost.go @@ -107,6 +107,11 @@ func lostPassword(w http.ResponseWriter, r *http.Request) { return } + if !validateAltcha(r) { + displayTmplError(w, http.StatusForbidden, "lost.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again."}) + return + } + // Connect to the LDAP server conn, err := myLDAP.Connect() if err != nil || conn == nil { diff --git a/main.go b/main.go index 843dadf..ec6e888 100644 --- a/main.go +++ b/main.go @@ -213,6 +213,8 @@ func main() { signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) // Register handlers + http.HandleFunc(fmt.Sprintf("GET %s/altcha.min.js", *baseURL), serveAltchaJS) + http.HandleFunc(fmt.Sprintf("GET %s/altcha-challenge", *baseURL), serveAltchaChallenge) http.HandleFunc(fmt.Sprintf("%s/{$}", *baseURL), changePassword) http.HandleFunc(fmt.Sprintf("POST %s/api/v1/aliases", *baseURL), addyAliasAPI) http.HandleFunc(fmt.Sprintf("DELETE %s/api/v1/aliases/{alias}", *baseURL), addyAliasAPIDelete) diff --git a/reset.go b/reset.go index c2172b0..a37f99d 100644 --- a/reset.go +++ b/reset.go @@ -44,6 +44,11 @@ func resetPassword(w http.ResponseWriter, r *http.Request) { return } + if !validateAltcha(r) { + renderError(http.StatusForbidden, "Invalid or missing altcha response. Please try again.") + return + } + // Check the two new passwords are identical if r.PostFormValue("newpassword") != r.PostFormValue("new2password") { renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.") diff --git a/static.go b/static.go index e808fe1..6fd5ff6 100644 --- a/static.go +++ b/static.go @@ -12,7 +12,7 @@ func securityHeaders(next http.Handler) http.Handler { w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline' https://stackpath.bootstrapcdn.com; style-src https://stackpath.bootstrapcdn.com; img-src 'self'; font-src https://stackpath.bootstrapcdn.com") + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://stackpath.bootstrapcdn.com; style-src https://stackpath.bootstrapcdn.com; img-src 'self'; font-src https://stackpath.bootstrapcdn.com") w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains") next.ServeHTTP(w, r) }) diff --git a/static/change.html b/static/change.html index 60ffe7e..424b171 100644 --- a/static/change.html +++ b/static/change.html @@ -40,6 +40,9 @@ +