Joplin Server: Stored XSS über inline ausgelieferten Notiz-Anhang auf öffentlichen Freigaben
Joplin Server liefert Anhänge öffentlich freigegebener Notizen mit einem vom Angreifer gesteuerten Content-Type inline aus. Ein Nutzer ohne Adminrechte lädt ein SVG mit leerem Titel hoch und führt beim Öffnen der öffentlichen Freigabe beliebiges JavaScript im Server-Origin aus, gegen einen eingeloggten Administrator bis hin zu vollem Admin-Zugriff.
Advisory-ID: TP-2026-060
Produkt: Joplin Server (Sync- und Freigabe-Backend der Open-Source-Notizanwendung Joplin, self-hosted)
Schwachstellentyp: Stored Cross-Site Scripting (CWE-79)
CVE: CVE-2026-59814
CVSS 3.1: 7.6 (Hoch) · CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N
Betroffene Versionen: <= 3.7.1
Behoben in: 3.7.2
Hersteller-Advisory: GHSA-mx98-7h4g-6gmh
Gemeldet: 23. Juni 2026
Zusammenfassung
Joplin Server liefert die Anhänge (Ressourcen) öffentlich publizierter Notizen über GET /shares/:id?resource_id= mit einem Content-Type aus, der direkt aus dem angreifer-kontrollierten mime-Feld der Ressource stammt. Die Route setzt Content-Disposition: attachment nur dann, wenn die Ressource einen nicht-leeren Titel hat. Eine Ressource, die mit leerem Titel und mime: image/svg+xml hochgeladen wird, wird inline ausgeliefert. Die Anwendung setzt für die gesamte Seite nur einen CSP-Header, frame-ancestors 'none', ohne script-src, ohne Sandbox und ohne X-Content-Type-Options: nosniff, sodass das inline gerenderte SVG sein eingebettetes Script im Web-Origin von Joplin Server ausführt. Ausgelieferter Nutzer-Content teilt sich standardmäßig den Anwendungs-Origin. Jeder authentifizierte Nicht-Admin lädt eine solche Ressource über die dokumentierte Sync-API hoch und publiziert die Notiz. Der öffentliche Link trägt die Payload zu jedem Betrachter.
Ursache
renderResource() gibt den ausgelieferten mime und filename direkt aus dem angreifer-hochgeladenen Ressourcen-Metadatensatz zurück, ohne MIME-Allowlist und mit optionalem Titel (packages/server/src/utils/joplinUtils.ts:206-211). Die öffentliche Freigabe-Route schreibt diesen angreifer-mime als Response-Content-Type und emittiert Content-Disposition nur, wenn result.filename truthy ist, sodass ein leerer Ressourcentitel eine inline Response erzeugt (packages/server/src/routes/index/shares.ts:57-60). Der einzige für die gesamte Anwendung gesetzte CSP ist frame-ancestors 'none', ohne script-src, ohne Sandbox und ohne X-Content-Type-Options: nosniff (packages/server/src/middleware/clickJackingHandler.ts:5), sodass ein inline ausgeliefertes image/svg+xml-Dokument sein Script ausführt. Ausgelieferter Nutzer-Content nutzt den Anwendungs-Origin, weil userContentBaseUrl standardmäßig auf baseUrl fällt (packages/server/src/config.ts:210,246). Das Publizieren einer Notiz trägt keine Berechtigungsprüfung, da ShareModel.checkIfAllowed nur Ordner-Freigaben einschränkt (packages/server/src/models/ShareModel.ts:23).
Proof of Concept
# 1. Als Nutzer ohne Adminrechte je via PUT /api/items/root:/<name>:/content
# (Header X-API-AUTH: <session>) hochladen: einen Ordner, eine Notiz mit
# Body , einen Ressourcen-Metadatensatz mit LEEREM Titel und
# mime image/svg+xml sowie das Ressourcen-Blob:
<svg xmlns="http://www.w3.org/2000/svg" onload="document.title=document.domain"><script>document.title=document.domain</script></svg>
# 2. Notiz publizieren:
POST /api/shares
{"note_id":"<noteId>"}
=> liefert die Freigabe-id zurueck
# 3. Als anonymer Client die Ressourcen-URL oeffnen:
GET /shares/<shareId>?resource_id=<resId>
HTTP/1.1 200 OK
Content-Security-Policy: frame-ancestors 'none'
Content-Type: image/svg+xml
(kein Content-Disposition, kein X-Content-Type-Options)
# Live verifiziert: echtes Chrome rendert das SVG-Dokument, fuehrt onload und
# das inline <script> aus und schreibt document.title im Joplin-Server-Origin.
# Mit eingeloggtem Administrator liest eine aequivalente Payload /admin/users
# (HTTP 200, volle Nutzerliste) und das Anti-CSRF-Token aus /admin/users/new
# unter dem Admin-Session-Cookie.
Auswirkung
- Beliebiges JavaScript läuft im Origin von Joplin Server im Browser jedes Betrachters einer publizierten Notiz.
- Gegen einen eingeloggten Administrator (live verifiziert): Das Script liest die Admin-Seite
/admin/users(HTTP 200, volle Nutzerliste samt E-Mail-Adressen) und das Anti-CSRF-Token aus/admin/users/newunter dem Admin-Session-Cookie, umgeht damit den CSRF-Schutz und ermöglicht schreibende Admin-Operationen (Nutzer anlegen oder sperren, Passwörter zurücksetzen, Identität übernehmen). - Gegen jeden eingeloggten Nutzer: Same-Origin-Lesen und -Ändern der Daten und des Kontos dieses Nutzers.
- Zum Platzieren genügt die niedrigste Nicht-Admin-Rolle in der Standardkonfiguration, Träger ist ein anonymer öffentlicher Freigabe-Link.
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.
