Overview
CVE-2026-33124 describes a Broken Authentication flaw in Frigate where versions prior to 0.17.0-beta1 allowed any authenticated user to change their own password via the /users/{username}/password endpoint without verifying the current password. Because the password change did not invalidate existing JWT tokens and there was no validation of password strength, an attacker who obtained a valid session token (via exposed JWT, cookie theft, XSS, compromised device, or network sniffing over HTTP) could change the victim’s password and retain permanent control of the account. Since tokens remained valid after a password change, session hijacking persisted even after resets. The lack of password strength validation also opened doors to brute-force attacks. The issue has been fixed in version 0.17.0-beta1. In Go (Gin) applications, this pattern manifests as an endpoint that updates the password without verifying the current password, failing to tie the change to the authenticated user, and not rotating or revoking tokens, leaving prior sessions usable after the change.
This guide explains what these vulnerabilities do, how they were exploited in real-world scenarios, and concrete Go (Gin) remediation steps. Key remediation patterns include enforcing current-password verification during password changes, ensuring the authenticated user is updating their own credentials, applying strong password policies, and rotating or revoking tokens on password changes to invalidate existing sessions. Implementing these mitigations within a Gin-based API typically involves careful integration of authentication middleware, secure password hashing, and a token strategy that supports revocation or rotation.
To prevent similar flaws, structure password update flows to require proof of knowledge (current password), enforce strict password quality, and adopt token-management practices that invalidate or rotate tokens upon credential changes. Include thorough input validation, avoid exposing password-handling logic in error messages, require transport over TLS, and implement auditing and monitoring for password-change events to detect anomalous activity.
Affected Versions
< 0.17.0-beta1
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"time"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
)
type User struct {
Username string
PasswordHash string
TokenVersion int
}
var jwtSecret = []byte("change-me-now-please")
var users = map[string]*User{
// initial user: alice with a strong password and token version 1
"alice": {Username: "alice", PasswordHash: hashPassword("StrongPassw0rd!"), TokenVersion: 1},
}
func hashPassword(pw string) string {
h, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
return string(h)
}
func verifyPassword(user *User, pw string) bool {
return bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(pw)) == nil
}
func generateToken(username string, tv int, duration time.Duration) (string, error) {
claims := jwt.MapClaims{
"sub": username,
"tv": tv,
"exp": time.Now().Add(duration).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
tokenStr := strings.TrimPrefix(auth, "Bearer ")
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
username, _ := claims["sub"].(string)
tvFloat, _ := claims["tv"].(float64)
c.Set("username", username)
c.Set("tokenVersion", int(tvFloat))
c.Next()
return
}
c.AbortWithStatus(http.StatusUnauthorized)
}
}
// VulnerableChangePassword demonstrates the insecure pattern: it accepts a new_password
// and updates the user without validating the current password, and without token rotation.
func VulnerableChangePassword(c *gin.Context) {
username := c.Param("username")
var req struct{ NewPassword string `json:"new_password"` }
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
user := users[username]
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
hashed, _ := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
user.PasswordHash = string(hashed)
// No current-password verification, no token rotation or invalidation of existing tokens
c.JSON(http.StatusOK, gin.H{"status": "password changed (vulnerable)"})
}
func validateStrength(pw string) bool {
if len(pw) < 12 {
return false
}
hasUpper, hasLower, hasDigit := false, false, false
for _, r := range pw {
switch {
case 'A' <= r && r <= 'Z':
hasUpper = true
case 'a' <= r && r <= 'z':
hasLower = true
case '0' <= r && r <= '9':
hasDigit = true
}
}
return hasUpper && hasLower && hasDigit
}
// SecureChangePassword fixes the vulnerability by:
// 1) requiring the authenticated user to match the target user,
// 2) verifying the current password,
// 3) enforcing password strength,
// 4) rotating tokens on password changes so old tokens become invalid.
func SecureChangePassword(c *gin.Context) {
usernameParam := c.Param("username")
usernameFromToken, _ := c.Get("username")
if usernameFromToken != usernameParam {
c.JSON(http.StatusForbidden, gin.H{"error": "cannot change another user's password"})
return
}
var req struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
user := users[usernameFromToken.(string)]
if user == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if !verifyPassword(user, req.CurrentPassword) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "current password incorrect"})
return
}
if !validateStrength(req.NewPassword) {
c.JSON(http.StatusBadRequest, gin.H{"error": "password does not meet strength requirements"})
return
}
hashed, _ := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
user.PasswordHash = string(hashed)
user.TokenVersion++ // rotate tokens so old tokens become invalid
newAccess, _ := generateToken(user.Username, user.TokenVersion, 15*time.Minute)
newRefresh, _ := generateToken(user.Username, user.TokenVersion, 7*24*time.Hour)
c.JSON(http.StatusOK, gin.H{"status": "password changed securely", "access_token": newAccess, "refresh_token": newRefresh})
}
func main() {
r := gin.Default()
// Vulnerable endpoint: does not verify current password or rotate tokens
r.POST("/users/:username/password", VulnerableChangePassword)
// Secure endpoint: requires authentication, validates current password, enforces strength, rotates tokens
auth := r.Group("", AuthMiddleware())
auth.POST("/secure/users/:username/password", SecureChangePassword)
r.Run(":8080")
}