chldapasswd/change.go
Pierre-Olivier Mercier 2a9eec233a fix(security): add per-IP rate limiting to all authentication endpoints
Implement sliding window rate limiter to prevent brute-force attacks:
- /auth and /login: 20 requests/minute per IP
- /change: 10 POST requests/minute per IP
- /lost: 5 POST requests/minute per IP (prevents email spam and user enumeration)
- /reset: 10 POST requests/minute per IP
- /api/v1/aliases: 30 requests/minute per IP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:30:48 +07:00

73 lines
2.7 KiB
Go

package main
import (
"errors"
"log"
"net/http"
)
func checkPasswdConstraint(password string) error {
if len(password) < 8 {
return errors.New("too short, please choose a password at least 8 characters long.")
}
return nil
}
func changePassword(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && !changeLimiter.Allow(remoteIP(r)) {
csrfToken, _ := setCSRFToken(w)
displayTmplError(w, http.StatusTooManyRequests, "change.html", map[string]interface{}{"error": "Too many requests. Please try again later.", "csrf_token": csrfToken})
return
}
if r.Method != "POST" {
csrfToken, err := setCSRFToken(w)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
displayTmpl(w, "change.html", map[string]interface{}{"csrf_token": csrfToken})
return
}
if !validateCSRF(r) {
csrfToken, _ := setCSRFToken(w)
displayTmplError(w, http.StatusForbidden, "change.html", map[string]interface{}{"error": "Invalid or missing CSRF token. 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})
}
// Check the two new passwords are identical
if r.PostFormValue("newpassword") != r.PostFormValue("new2password") {
renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.")
} else if len(r.PostFormValue("login")) == 0 {
renderError(http.StatusNotAcceptable, "Please provide a valid login")
} else if err := checkPasswdConstraint(r.PostFormValue("newpassword")); err != nil {
renderError(http.StatusNotAcceptable, "The password you chose doesn't respect all constraints: "+err.Error())
} else {
conn, err := myLDAP.Connect()
if err != nil || conn == nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
} else if err := conn.ServiceBind(); err != nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
} else if dn, err := conn.SearchDN(r.PostFormValue("login"), true); err != nil {
log.Println(err)
renderError(http.StatusUnauthorized, "Invalid login or password.")
} else if err := conn.Bind(dn, r.PostFormValue("password")); err != nil {
log.Println(err)
renderError(http.StatusUnauthorized, "Invalid login or password.")
} else if err := conn.ChangePassword(dn, r.PostFormValue("newpassword")); err != nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
} else {
displayMsg(w, "Password successfully changed!", http.StatusOK)
}
}
}