Overview
CVE-2026-41571 describes a Broken Authentication vulnerability in Note Mark where, in v0.19.2, IsPasswordMatch falls back to a hard-coded bcrypt("null") placeholder when a user has no stored password. Since OIDC-registered users are created with empty passwords, attackers can submit password: "null" to the internal login endpoint and obtain a valid session. This unauthenticated bypass is fixed in v0.19.3. This pattern maps to CWE-287 (Improper Authentication). In Go with Gin, the vulnerability typically occurs when the code treats missing passwords as if they were valid by comparing the provided password against a static placeholder hash, enabling login without a real credential. The result is an authentication bypass that undermines identity verification and session integrity.
Affected Versions
0.19.2
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
type User struct {
Username string
PasswordHash string
}
var users map[string]*User
var placeholderHash string
func init() {
// Placeholder hash corresponds to the password "null" used in vulnerable code paths
ph, _ := bcrypt.GenerateFromPassword([]byte("null"), bcrypt.DefaultCost)
placeholderHash = string(ph)
users = map[string]*User{
"alice": {Username: "alice", PasswordHash: ""}, // no password configured
"bob": {Username: "bob", PasswordHash: hashOf("s3cr3t")}, // proper password
}
}
func hashOf(pw string) string {
h, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
return string(h)
}
func main() {
r := gin.Default()
r.POST("/vuln/login", vulnerableLogin)
r.POST("/fix/login", fixedLogin)
_ = r.Run(":8080")
}
func vulnerableLogin(c *gin.Context) {
type cred struct { Username string; Password string }
var in cred
if err := c.BindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad"})
return
}
u := users[in.Username]
if u == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
hash := u.PasswordHash
if hash == "" {
// Vulnerable: fall back to placeholder hash for empty passwords
hash = placeholderHash
}
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(in.Password)) != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "logged_in"})
}
func fixedLogin(c *gin.Context) {
type cred struct { Username string; Password string }
var in cred
if err := c.BindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad"})
return
}
u := users[in.Username]
if u == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
// Fixed: do not allow login when password is not configured
if u.PasswordHash == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "no password configured"})
return
}
if bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(in.Password)) != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "logged_in"})
}