ZenNotes Server: Vault Access from Any Other Loopback Port

A page on another port of the same loopback host name reads every note in cleartext and writes, renames and permanently deletes, using the logged-in victim's session.

Advisory ID: TP-2026-126
Product: ZenNotes Server (self-hosted server with web client for the ZenNotes Markdown notes app)
Vulnerability type: Permissive CORS policy (CWE-942) and cross-site request forgery (CWE-352)
CVE: not requested
CVSS 3.1: 6.3 (Medium) · CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Affected versions: <= 2.83.0
Vendor advisory: GHSA-f33v-gwvr-3j4g
Fixed in: 2.83.1
Reported: 28 October 2026

Summary

ZenNotes Server serves a notes vault together with a web client and binds to 127.0.0.1:7878 by default. On a loopback bind it answers every loopback origin with a credentialed CORS grant, and the SameSite=Strict session cookie does not help because the port is not part of the site. State-changing endpoints check neither a CSRF token nor Sec-Fetch nor the content type. A page on another port of the same loopback host name therefore reads every note in cleartext and writes, renames, uploads and permanently deletes, using the logged-in victim's session.

Root cause

originDecisionFor returns allowed and credentials for every loopback origin once the server's own bind is a loopback bind, and corsMiddleware echoes that origin with Access-Control-Allow-Credentials: true (internal/httpserver/security.go:375-376, :386-397). The compiled-in default bind is 127.0.0.1:7878 (internal/config/config.go:75). The session cookie carries HttpOnly; SameSite=Strict, and SameSite separates host labels, not ports, so requests from a page on another loopback port carry the cookie (internal/httpserver/security.go:476-477). requireAuth is the only authorization middleware in front of the state-changing routes and accepts the bearer header or the session cookie without identifying the caller (internal/httpserver/server.go:261, :270-271). readJSON decodes the body without a content-type check, so a form with enctype="text/plain" reaches /api/notes/write and /api/notes/delete with no preflight (internal/httpserver/server.go:384-386, :207, :211).

Proof of Concept

Server: 127.0.0.1:7878 (default bind), token set, victim logged in

$ curl -si -b cookies.txt -H 'Origin: http://127.0.0.1:9999' http://127.0.0.1:7878/api/notes
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://127.0.0.1:9999
Access-Control-Allow-Credentials: true

// page on http://127.0.0.1:9999
fetch('http://127.0.0.1:7878/api/notes/read?path=inbox/Payroll.md',{credentials:'include'})
new WebSocket('ws://127.0.0.1:7878/api/watch')

<form method=POST enctype="text/plain" action="http://127.0.0.1:7878/api/notes/delete">
<input name='{"path":"inbox/FormDel.md","pad":"' value='"}'>
</form>

The page on 127.0.0.1:9999 is same-site with the instance on 127.0.0.1:7878, so the cookie rides along, and the CORS grant lets it read the response. The control from http://localhost:9999 gets 401; the form deletes the note with no copy in trash/ and also works on the Docker bind 0.0.0.0:7878, where the CORS branch does not fire.

Impact

  • Cleartext disclosure of every note body, title, tag and uploaded asset to a page the victim only opens.
  • Write, rename, overwrite and asset upload in the vault, plus permanent deletion with no copy in the trash.
  • Live feed of every subsequent vault change through /api/watch.
  • The attacker needs no token and no account and borrows the victim's session; the precondition is a page on another port of the same loopback host name.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.