ZenNotes Server: Vault-Zugriff von jedem anderen Loopback-Port
Eine Seite auf einem anderen Port derselben Loopback-Hostbezeichnung liest jede Notiz im Klartext und schreibt, benennt um und löscht dauerhaft, mit der Sitzung des angemeldeten Opfers.
Advisory-ID: TP-2026-126
Produkt: ZenNotes Server (selbst gehosteter Server mit Web-Client für die Markdown-Notizen-App ZenNotes)
Schwachstellentyp: Freizügige CORS-Richtlinie (CWE-942) und Cross-Site Request Forgery (CWE-352)
CVE: nicht beantragt
CVSS 3.1: 6.3 (Mittel) · CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Betroffene Versionen: <= 2.83.0
Hersteller-Advisory: GHSA-f33v-gwvr-3j4g
Behoben in: 2.83.1
Gemeldet: 28. Oktober 2026
Zusammenfassung
ZenNotes Server liefert einen Notiz-Vault samt Web-Client aus und bindet sich im Auslieferungszustand an 127.0.0.1:7878. Auf einem Loopback-Bind beantwortet er jede Loopback-Herkunft mit einer CORS-Freigabe inklusive Anmeldedaten, und der Sitzungscookie mit SameSite=Strict schützt nicht, weil der Port nicht zur Site gehört. Zustandsändernde Endpunkte prüfen weder CSRF-Token noch Sec-Fetch noch den Content-Type. Eine Seite auf einem anderen Port derselben Loopback-Hostbezeichnung liest damit jede Notiz im Klartext und schreibt, benennt um, lädt hoch und löscht dauerhaft, mit der Sitzung des angemeldeten Opfers.
Ursache
originDecisionFor gibt für jede Loopback-Herkunft allowed und credentials zurück, sobald der eigene Bind ein Loopback-Bind ist, und corsMiddleware spiegelt diese Herkunft mit Access-Control-Allow-Credentials: true (internal/httpserver/security.go:375-376, :386-397). Der fest einkompilierte Standard-Bind ist 127.0.0.1:7878 (internal/config/config.go:75). Der Sitzungscookie trägt HttpOnly; SameSite=Strict, und SameSite trennt Hostbezeichnungen, nicht Ports, sodass Anfragen einer Seite auf einem anderen Loopback-Port den Cookie mitsenden (internal/httpserver/security.go:476-477). requireAuth ist die einzige Autorisierungs-Middleware vor den zustandsändernden Routen und akzeptiert Bearer-Header oder Sitzungscookie, ohne den Aufrufer zu identifizieren (internal/httpserver/server.go:261, :270-271). readJSON dekodiert den Rumpf ohne Prüfung des Content-Type, sodass ein Formular mit enctype="text/plain" ohne Preflight /api/notes/write und /api/notes/delete erreicht (internal/httpserver/server.go:384-386, :207, :211).
Proof of Concept
Server: 127.0.0.1:7878 (Standard-Bind), Token gesetzt, Opfer ist angemeldet
$ 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
// Seite auf 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>
Die Seite auf 127.0.0.1:9999 ist zur Instanz auf 127.0.0.1:7878 gleiche Site, deshalb fährt der Cookie mit, und die CORS-Freigabe lässt sie die Antwort lesen. Die Gegenprobe von http://localhost:9999 erhält 401; das Formular löscht die Notiz ohne Kopie in trash/ und wirkt auch auf dem Docker-Bind 0.0.0.0:7878, auf dem der CORS-Zweig nicht greift.
Auswirkung
- Offenlegung aller Notizinhalte, Titel, Tags und hochgeladenen Anhänge im Klartext an eine Seite, die das Opfer nur öffnet.
- Schreiben, Umbenennen, Überschreiben und Hochladen im Vault sowie dauerhaftes Löschen ohne Kopie im Papierkorb.
- Live-Mitschnitt jeder weiteren Vault-Änderung über
/api/watch. - Der Angreifer braucht weder Token noch Konto, er nutzt die Sitzung des Opfers; Voraussetzung ist eine Seite auf einem anderen Port derselben Loopback-Hostbezeichnung.
Referenzen
Steckt so etwas in Ihrer Software?
Diese Schwachstelle hat unser Team im Rahmen seiner Arbeit gefunden. Lassen Sie Ihre Anwendungen von denselben Spezialisten prüfen, mit einem Penetrationstest von turingpoint.
