fix(security): redesign password reset tokens using crypto/rand with server-side storage
- Replace SHA512-based deterministic token with 32-byte crypto/rand token - Store tokens server-side with 1-hour expiry and single-use semantics - Remove genToken (previously broken due to time.Add immutability bug) - Add CSRF double-submit cookie protection to change/lost/reset forms - Remove token from form action URL (use hidden fields only, POST body) - Add MailFrom field and SMTP_FROM env var for configurable sender address - Add SMTP_PASSWORD_FILE env var for secure SMTP password loading - Add PUBLIC_URL env var and --public-url flag for configurable reset link domain - Use generic error messages in handlers to avoid information disclosure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a2f368eb02
commit
57775bbf89
9 changed files with 193 additions and 83 deletions
62
reset.go
62
reset.go
|
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func resetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -14,22 +13,46 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
base := map[string]interface{}{
|
||||
"login": r.URL.Query().Get("l"),
|
||||
"token": strings.Replace(r.URL.Query().Get("t"), " ", "+", -1),
|
||||
"token": r.URL.Query().Get("t"),
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
csrfToken, err := setCSRFToken(w)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
base["csrf_token"] = csrfToken
|
||||
displayTmpl(w, "reset.html", base)
|
||||
return
|
||||
}
|
||||
|
||||
renderError := func(status int, msg string) {
|
||||
csrfToken, _ := setCSRFToken(w)
|
||||
base["error"] = msg
|
||||
base["csrf_token"] = csrfToken
|
||||
displayTmplError(w, status, "reset.html", base)
|
||||
}
|
||||
|
||||
if !validateCSRF(r) {
|
||||
renderError(http.StatusForbidden, "Invalid or missing CSRF token. Please try again.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check the two new passwords are identical
|
||||
if r.PostFormValue("newpassword") != r.PostFormValue("new2password") {
|
||||
base["error"] = "New passwords are not identical. Please retry."
|
||||
displayTmplError(w, http.StatusNotAcceptable, "reset.html", base)
|
||||
renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.")
|
||||
return
|
||||
} else if err := checkPasswdConstraint(r.PostFormValue("newpassword")); err != nil {
|
||||
base["error"] = "The password you chose doesn't respect all constraints: " + err.Error()
|
||||
displayTmplError(w, http.StatusNotAcceptable, "reset.html", base)
|
||||
renderError(http.StatusNotAcceptable, "The password you chose doesn't respect all constraints: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and consume the token (single-use, server-side)
|
||||
token := r.PostFormValue("token")
|
||||
dn, ok := consumeResetToken(token)
|
||||
if !ok {
|
||||
renderError(http.StatusNotAcceptable, "Token invalid or expired, please retry the lost password procedure. Tokens expire after 1 hour.")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -37,41 +60,22 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
|
|||
conn, err := myLDAP.Connect()
|
||||
if err != nil || conn == nil {
|
||||
log.Println(err)
|
||||
base["error"] = err.Error()
|
||||
displayTmplError(w, http.StatusInternalServerError, "reset.html", base)
|
||||
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
// Bind as service to perform the search
|
||||
// Bind as service to perform the password change
|
||||
err = conn.ServiceBind()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
base["error"] = err.Error()
|
||||
displayTmplError(w, http.StatusInternalServerError, "reset.html", base)
|
||||
return
|
||||
}
|
||||
|
||||
// Search the dn of the given user
|
||||
dn, err := conn.SearchDN(r.PostFormValue("login"), true)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
base["error"] = err.Error()
|
||||
displayTmplError(w, http.StatusInternalServerError, "reset.html", base)
|
||||
return
|
||||
}
|
||||
|
||||
// Check token validity (allow current token + last one)
|
||||
if conn.genToken(dn, false) != r.PostFormValue("token") && conn.genToken(dn, true) != r.PostFormValue("token") {
|
||||
base["error"] = "Token invalid, please retry the lost password procedure. Please note that our token expires after 1 hour."
|
||||
displayTmplError(w, http.StatusNotAcceptable, "reset.html", base)
|
||||
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
// Replace the password by the new given
|
||||
if err := conn.ChangePassword(dn, r.PostFormValue("newpassword")); err != nil {
|
||||
log.Println(err)
|
||||
base["error"] = err.Error()
|
||||
displayTmplError(w, http.StatusInternalServerError, "reset.html", base)
|
||||
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue