
Docmost akzeptierte eine javascript:-URL innerhalb eines Anhänge-Knotens, bewahrte sie durch Speicherung und Rendering und verwandelte sie zurück in einen anklickbaren Anker im Docmost-Ursprung.
Docmost akzeptierte eine javascript:-URL innerhalb eines Attachment-Knotens, bewahrte sie bei Speicherung und Darstellung unverändert und verwandelte sie in einen anklickbaren Anchor im Docmost-Origin.
Ich habe eine gespeicherte XSS mit hohem Schweregrad in Docmost, der quelloffenen kollaborativen Dokumentationsplattform, identifiziert, verantwortungsvoll offengelegt und reproduziert.
Die offizielle Seite von Docmost präsentiert es als unternehmensreifes On-Premises-Wiki mit über 3 Mio. Downloads und gibt an, dass Teams von Organisationen wie der Stadt Vilnius, Bechtle, der australischen Regierung, dem Roten Kreuz und ETS Quebec darauf vertrauen.
Der Fehler saß an einer Stelle, die in Rich-Text-Systemen leicht zu übersehen ist:
nicht in der normalen Link-Erweiterung, sondern in einem separaten benutzerdefinierten Knotentyp für Dateianhänge.
Ich habe die Editor-Pipeline mit einer sehr spezifischen Frage überprüft:
Wenn normale Links javascript:-URLs blockieren, setzen Attachment-Knoten dieselbe Regel durch, bevor sie einen Anchor-Sink erreichen?
In verwundbaren Versionen taten sie das nicht.
Docmost akzeptierte einen bösartigen Attachment-Knoten in der JSON-Seite, speicherte sein url-Attribut unverändert und rendete diesen Wert später zurück in ein klickbares <a href="javascript:...">-Element.
Dieser Fehler wurde zu CVE-2026-34212.
Docmost: docmost/docmost
Advisory: GHSA-cf68-cff9-hq4w
CVE: CVE-2026-34212
Behoben in: v0.71.0
Angreifer-kontrollierte Attachment-Knoten-URL -> Seiten-JSON wird akzeptiert und unverändert gespeichert -> HTML/React-Rendering wandelt diese URL in einen Anchor-Href um -> Opfer klickt auf Attachment-Aktion -> Angreifer-kontrolliertes JavaScript wird im Docmost-Origin ausgeführt
Docmost speichert Seiteninhalte in einem ProseMirror/Tiptap-kompatiblen JSON-Format.
Dieses Inhaltsmodell enthält benutzerdefinierte Block-Knoten für Dinge wie:
Der Attachment-Knoten speichert Felder wie:
urlnamemimesizeattachmentIdDer Server akzeptiert Seiteninhalte in mehreren Formaten:
jsonmarkdownhtmlund normalisiert sie in ProseMirror-JSON, bevor er sie speichert.
Das bedeutet, dass jeder Knotentyp, der eine URL tragen kann, Teil einer direkten Vertrauensgrenze ist.
Wenn einer dieser Knotentypen schließlich in einen <a href> rendert, ist die URL-Schemabehandlung keine Option.
Sie ist Teil des Sicherheitsmodells.
Benutzerdefinierte Editor-Erweiterungen sind eine häufige Quelle für Sicherheitsabweichungen.
Das Basissystem mag bereits wissen, wie man gefährliche URLs korrekt behandelt, aber jeder benutzerdefinierte Knoten muss die gleichen Regeln an seinen eigenen Sinks erneut anwenden.
Das schafft eine vorhersehbare Überprüfungsstrategie:
Genau das hat diesen Fehler aufgedeckt.
Docmosts normale Link-Erweiterung behandelte javascript: bereits als gefährlich.
Sein Attachment-Knoten tat das nicht.
Sobald man diese Asymmetrie sieht, wird die Sicherheitsfrage offensichtlich:
Kann ich einen Attachment-Knoten persistieren, dessen url javascript: ist, und ihn zurück in einen live Anchor rendern lassen?
Die Antwort war ja.
Die Grundursache war inkonsistente URL-Bereinigung über Inhaltsknotentypen hinweg.
Der serverseitige Inhaltsweg akzeptierte beliebige Attachment-URLs, solange der Gesamtinhalt dem ProseMirror-Schema entsprach.
In der verwundbaren Version:
CreatePageDto akzeptierte content?: string | objectPageService.parseProsemirrorContent() normalisierte markdown, html oder jsonjsonToNode(prosemirrorJson) aufDieser Validierungsschritt überprüfte die strukturelle Gültigkeit, nicht die URL-Sicherheit.
Der kritische Teil der verwundbaren Serverlogik war effektiv:
prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;
Dort fand keine Normalisierung des Attachment-URL-Schemas statt.
Später rendete die Attachment-Erweiterung den angreiferkontrollierten Wert direkt.
Der verwundbare Attachment-Knoten tat dies:
url: {
default: "",
parseHTML: (element) => element.getAttribute("data-attachment-url"),
renderHTML: (attributes) => ({
"data-attachment-url": attributes.url,
}),
},
und dann:
[
"a",
{
href: HTMLAttributes["data-attachment-url"],
class: "attachment",
target: "blank",
},
`${HTMLAttributes["data-attachment-name"]}`,
]
Auf der Client-Seite hat die React-Knotenansicht dies erneut eingewickelt in:
<a href={getFileUrl(url)} target="_blank">
Aber getFileUrl() behandelte nur Sonderfälle:
http-URLs/api/.../files/...Alles andere wurde unverändert zurückgegeben.
So überlebte ein Payload wie:
javascript:alert(document.domain)
Das allein wäre bereits ausreichend für eine gespeicherte XSS gewesen.
Was die Grundursache besonders deutlich macht, ist der Vergleichspunkt.
Docmosts normale Link-Erweiterung blockierte javascript: explizit:
javascript: in parseHTML() abjavascript:-href in renderHTML() auf leerDas Produkt wusste also bereits, dass dieses Schema gefährlich war.
Der Attachment-Knoten versäumte es einfach, die gleiche Richtlinie anzuwenden.
Deshalb war dies keine "generische XSS im Editor."
Es war eine knotenspezifische Vertrauensgrenzlücke.
Dieser Fehler betraf nicht nur unästhetisches, unsicheres HTML.
Er erlaubte einem Angreifer, der eine Seite bearbeiten konnte, einen bösartigen Payload zu persistieren, der später im Docmost-Origin ausgeführt wurde, wenn ein anderer Benutzer mit dem gerenderten Attachment interagierte.
Das ist wichtig, weil Code im Origin Folgendes kann:
Die Notwendigkeit eines Klicks reduziert dies nicht auf ein triviales Problem.
Der Klick ist Teil des normalen Produktverhaltens: Die UI präsentiert das Attachment absichtlich als handlungsrelevanten Link/Icon.
Die Sicherheitsfrage ist also nicht "Kann der Angreifer ohne Interaktion beliebiges JS erzwingen?"
Die eigentliche Frage ist:
Speichert die Anwendung skripttragende, angreiferkontrollierte Inhalte und präsentiert sie später anderen Benutzern als vertrauenswürdigen Interaktionsweg?
In verwundbaren Versionen tat sie das.
Das ist gespeicherte XSS.
Der Ausnutzungsweg war unkompliziert: