
Docmost ha accettato un URL javascript: all'interno di un nodo allegato, lo ha preservato attraverso l'archiviazione e il rendering, e lo ha ritrasformato in un'ancora cliccabile nell'origine di Docmost.
Docmost accettava un URL javascript: all'interno di un nodo allegato, lo conservava attraverso lo storage e il rendering, e lo trasformava di nuovo in un anchor cliccabile nell'origine di Docmost.
Ho identificato, divulgato responsabilmente e riprodotto un problema di XSS memorizzato ad alta gravità in Docmost, la piattaforma di documentazione collaborativa open-source.
Il sito ufficiale di Docmost la presenta come un wiki on-premises pronto per l'uso enterprise con oltre 3 milioni di download, e afferma che è utilizzata da team di organizzazioni tra cui Vilnius City, Bechtle, il Governo Australiano, la Croce Rossa e ETS Quebec.
Il bug si trovava in un punto facile da trascurare nei sistemi rich-text:
non nell'estensione dei link ordinari, ma in un tipo di nodo personalizzato separato usato per gli allegati di file.
Stavo esaminando la pipeline dell'editor con una domanda molto specifica in mente:
se i link normali bloccano gli URL javascript:, i nodi allegato applicano la stessa regola prima di raggiungere un sink di anchor?
Nelle versioni vulnerabili, non lo facevano.
Docmost accettava un nodo allegato malevolo nel JSON della pagina, conservava il suo attributo url inalterato, e successivamente rendeva quel valore in un elemento <a href="javascript:..."> cliccabile.
Quel problema è diventato CVE-2026-34212.
Docmost: docmost/docmost
Advisory: GHSA-cf68-cff9-hq4w
CVE: CVE-2026-34212
Corretto in: v0.71.0
URL del nodo allegato controllato dall'attaccante -> JSON della pagina accettato e memorizzato inalterato -> rendering HTML/React trasforma quell'URL in href di anchor -> la vittima clicca sull'azione dell'allegato -> JavaScript controllato dall'attaccante viene eseguito nell'origine di Docmost
Docmost memorizza il contenuto della pagina in un formato JSON compatibile con ProseMirror/Tiptap.
Questo modello di contenuto include nodi blocco personalizzati per cose come:
Il nodo allegato memorizza campi come:
urlnamemimesizeattachmentIdIl server accetta il contenuto della pagina in diversi formati:
jsonmarkdownhtmle lo normalizza in JSON ProseMirror prima di memorizzarlo.
Ciò significa che ogni tipo di nodo che può trasportare un URL fa parte di un confine di fiducia diretto.
Se uno di questi tipi di nodo alla fine viene reso in un <a href>, la gestione dello schema URL non è opzionale.
Fa parte del modello di sicurezza.
Le estensioni personalizzate dell'editor sono una frequente fonte di deriva di sicurezza.
Il sistema di base può già sapere come gestire correttamente gli URL pericolosi, ma ogni nodo personalizzato deve comunque riapplicare le stesse regole ai propri sink.
Questo crea una strategia di revisione prevedibile:
È esattamente quello che ha esposto questo bug.
La normale estensione dei link di Docmost già trattava javascript: come pericoloso.
Il suo nodo allegato no.
Una volta vista questa asimmetria, la domanda di sicurezza diventa ovvia:
posso persistere un nodo allegato il cui url è javascript: e ottenerlo renderizzato di nuovo in un anchor attivo?
La risposta è stata sì.
La causa principale era una sanitizzazione inconsistente degli URL tra i tipi di nodo di contenuto.
Il percorso del contenuto lato server accettava URL arbitrari degli allegati purché il contenuto complessivo corrispondesse allo schema ProseMirror.
Nella versione vulnerabile:
CreatePageDto accettava content?: string | objectPageService.parseProsemirrorContent() normalizzava markdown, html, o jsonjsonToNode(prosemirrorJson)Quel passaggio di validazione verificava la validità strutturale, non la sicurezza dell'URL.
La parte critica della logica del server vulnerabile era essenzialmente:
prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;
}
Non c'era normalizzazione dello schema URL dell'allegato.
Successivamente, l'estensione dell'allegato renderizzava direttamente il valore controllato dall'attaccante.
Il nodo allegato vulnerabile faceva questo:
```ts
url: {
default: "",
parseHTML: (element) => element.getAttribute("data-attachment-url"),
renderHTML: (attributes) => ({
"data-attachment-url": attributes.url,
}),
},
e poi:
[
"a",
{
href: HTMLAttributes["data-attachment-url"],
class: "attachment",
target: "blank",
},
`${HTMLAttributes["data-attachment-name"]}`,
]
Lato client, la vista del nodo React lo rimpacchettava di nuovo in:
<a href={getFileUrl(url)} target="_blank">
Ma getFileUrl() specializzava solo:
http assoluti/api/.../files/...Tutto il resto veniva restituito inalterato.
Quindi un payload come:
javascript:alert(document.domain)
sopravviveva a:
Questo da solo sarebbe già sufficiente per XSS memorizzato.
Ciò che rende la causa principale particolarmente chiara è il punto di confronto.
La normale estensione dei link di Docmost bloccava esplicitamente javascript::
javascript: in parseHTML()javascript: in renderHTML()Quindi il prodotto sapeva già che questo schema era pericoloso.
Il nodo allegato semplicemente non applicava la stessa politica.
Ecco perché questo non era "XSS generico nell'editor."
Era un gap nel confine di fiducia specifico del nodo.
Questo bug non riguardava solo un'estetica HTML non sicura.
Permetteva a un attaccante in grado di modificare una pagina di persistere un payload malevolo che successivamente sarebbe stato eseguito nell'origine di Docmost quando un altro utente interagiva con l'allegato renderizzato.
Questo è importante perché uno script in-origine può:
Il requisito di un clic non riduce questo a un problema banale.
Il clic fa parte del comportamento normale del prodotto: l'interfaccia utente presenta intenzionalmente l'allegato come un link/icona cliccabile.
Quindi la domanda di sicurezza non è "l'attaccante può forzare JS arbitrario senza alcuna interazione?"
La domanda reale è:
l'applicazione memorizza contenuti malevoli portatori di script controllati dall'attaccante e li presenta successivamente ad altri utenti come un percorso di interazione fidato?
Nelle versioni vulnerabili, lo faceva.
Questo è XSS memorizzato.
Il percorso di sfruttamento era semplice:
Questo rendeva anche gli utenti con privilegi più elevati obiettivi realistici.
Se un proprietario del workspace, un amministratore o un editor ampiamente fidato visualizzava contenuti controllati dall'attaccante e cliccava sull'azione dell'allegato, lo script dell'attaccante veniva eseguito nel contesto di sessione con privilegi più elevati.
Questo è il punto pratico importante:
il requisito di privilegio dell'attaccante era solo basso. Il livello di privilegio della vittima determinava quanto valore la sessione XSS portava.
Ho validato il problema dal vivo contro Docmost v0.70.3.
La PoC utilizzava solo normali richieste HTTP e le API della pagina dell'applicazione.
Il flusso era:
POST /api/pages/update con format: "json" e un nodo allegato il cui url è un payload javascript:.POST /api/pages/info.href è ancora javascript:....Il contenuto malevolo minimo era:
{
"pageId": "<pageId>",
"content": {
"type": "doc",
"content": [
{
"type": "attachment",
"attrs": {
"url": "javascript:alert(document.domain)",
"name": "policy.pdf",
"mime": "application/pdf",
"size": 1
}
}
]
},
"operation": "replace",
"format": "json"
}
Il risultato live osservato dal mio test è stato:
019d18cf-4212-70b0-894a-fe20080fb0f1POST /api/pages/info restituiva il JSON memorizzato con:"url": "javascript:alert(document.domain)"
POST /api/pages/info con format: "html" restituiva HTML contenente:<div data-type="attachment" data-attachment-url="javascript:alert(document.domain)" data-attachment-name="policy.pdf" data-attachment-mime="application/pdf" data-attachment-size="1"><a href="javascript:alert(document.domain)" class="attachment" target="blank">policy.pdf</a></div>
Quella risposta HTML è la prova critica.
Non avevo bisogno di affidarmi a un'affermazione vaga del tipo "un browser potrebbe fare qualcosa di interessante."
L'applicazione stessa rendeva l'esatto sink eseguibile.
Una volta che un utente clicca su quel link/icona dell'allegato, il browser esegue l'URL javascript: nell'origine della pagina che lo ha creato.
Per XSS guidati dall'editor, gli screenshot da soli sono prove deboli.
Mostrano sintomi, non il fallimento del confine.
Ecco perché ho strutturato la PoC attorno a due checkpoint espliciti:
La prova di archiviazione mostrava che il server accettava e conservava lo schema pericoloso.
La prova di sink renderizzato mostrava che l'applicazione trasformava quel valore memorizzato di nuovo in:
<a href="javascript:...">
Questa suddivisione è importante.
Se un prodotto memorizza input pericolosi ma li neutralizza prima di ogni sink, puoi avere un gap di hardening ma non necessariamente un XSS attivo.
Se il prodotto memorizza input pericolosi e successivamente li renderizza in un sink di esecuzione reale, hai la catena completa della vulnerabilità.
Questo è ciò che è successo qui.
La correzione è stata rilasciata in v0.71.0 e ha affrontato il percorso di sfruttamento renderizzato applicando la sanitizzazione degli URL agli URL degli allegati.
L'estensione dell'allegato ora importa e utilizza sanitizeUrl, includendo:
data-attachment-url durante il parsingdata-attachment-url durante il renderinghref dell'anchorConcettualmente, la patch ha cambiato il nodo allegato da:
a:
Anche l'helper lato client getFileUrl() è stato aggiornato in modo che schemi sconosciuti non passino più inalterati.
Nella versione corretta, il percorso di fallback restituisce sanitizeUrl(src) invece di restituire src così com'è.
Questa è una parte importante della correzione perché il design vulnerabile aveva due problemi che si rinforzavano a vicenda:
href grezzoLa patch ha rimosso entrambe le assunzioni.
Questa è stata una buona correzione per il percorso XSS attivo perché ha riallineato la gestione degli URL degli allegati con il resto del modello di sicurezza dell'editor.
Detto questo, c'è ancora una lezione di hardening più ampia:
la sanitizzazione lato client o al momento del rendering è necessaria qui, ma il rifiuto lato server di schemi pericolosi durante la creazione/aggiornamento della pagina sarebbe un invariante ancora più forte.
Il modello più sicuro a lungo termine è:
La difesa in profondità è importante nei sistemi rich-content.
Per una copertura a lungo termine, questi sono i casi che contano di più:
attachment.attrs.url = "javascript:..."data-attachment-url="javascript:..."href="javascript:..."/api/files/... e /files/... devono continuare a funzionare normalmenteIl punto chiave è la coerenza.
Se i link normali sono sanitizzati ma i nodi URL personalizzati no, l'editor non ha realmente una politica di sicurezza URL unica.
Ha frammenti, e i frammenti sono dove vivono i bug XSS.
L'advisory pubblicato ha classificato questo problema come:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N
Questo si attesta a 7.6 / Alta.
Questa è una classificazione difendibile.
Le proprietà importanti sono:
L'interazione dell'utente rimane richiesta perché la vittima deve attivare il link/icona dell'allegato.
Ecco perché UI:R è corretto.
Ma una volta che si verifica quell'interazione, il confine di sicurezza è già fallito molto prima: l'applicazione ha memorizzato uno schema pericoloso e lo ha renderizzato di nuovo in un sink di esecuzione.
Ho segnalato il problema privatamente tramite GitHub Security Advisories con:
Il problema è stato accettato, è stato assegnato CVE-2026-34212 e pubblicato il 14 aprile 2026.
L'advisory pubblico attualmente elenca:
0.70.30.71.0La mia validazione dal vivo è stata eseguita su v0.70.3, che corrispondeva alla versione vulnerabile pubblicata.
La lezione principale qui non è semplicemente "sanitizza gli URL."
Lo sanno già tutti.
La lezione più interessante è:
se un'applicazione ha un tipo di nodo URL sicuro e un tipo di nodo URL non sicuro, quello non sicuro è la politica reale.
I sistemi rich-text spesso accumulano estensioni personalizzate più velocemente di quanto accumulino revisioni di sicurezza.
Questo crea esattamente questo tipo di asimmetria:
Questo bug mostra anche perché la validazione dello schema non è sufficiente.
jsonToNode() verificava che il contenuto fosse dati ProseMirror strutturalmente validi.
Non dimostrava che il contenuto fosse sicuro da renderizzare.
Queste sono domande diverse.
La revisione della sicurezza diventa molto più nitida quando si tengono separate queste domande:
Il nodo allegato ha superato la prima domanda e fallito la terza.
Ecco come i bug di contenuto memorizzato sopravvivono all'interno di pipeline editor altrimenti ben strutturate.
data-attachment-url e l'href dell'anchor direttamente dall'input controllato dall'attaccante.getFileUrl() restituiva schemi sconosciuti inalterati.javascript:, ma i nodi allegato no.v0.71.0 ha aggiunto la gestione sanitizeUrl al nodo allegato e al percorso di fallback client.Questa vulnerabilità non riguardava una stranezza del browser.
Riguardava un nodo di contenuto personalizzato che bypassava le assunzioni di sicurezza URL della propria applicazione.
Docmost accettava un URL di allegato controllato dall'attaccante, lo conservava attraverso lo storage, e poi lo renderizzava di nuovo in un anchor attivo nell'origine dell'applicazione.
Ecco perché è diventato CVE-2026-34212.
La correzione in v0.71.0 ha chiuso pulitamente il percorso XSS attivo, ma la lezione più ampia è quella che vale la pena conservare:
nelle applicazioni incentrate sull'editor, ogni nodo personalizzato che può trasportare un URL è un confine di sicurezza a sé stante, e deve essere revisionato come tale.