
Un utente Docmost con privilegi bassi potrebbe fornire un attachmentId vittima all'endpoint di upload generico e sovrascrivere l'allegato memorizzato di un'altra pagina all'interno dello stesso spazio di lavoro.
Un utente Docmost con privilegi ridotti potrebbe fornire un attachmentId vittima all'endpoint di upload generico e sovrascrivere un allegato memorizzato di un'altra pagina nello stesso spazio di lavoro.
Ho identificato, divulgato in modo responsabile e riprodotto un difetto di autorizzazione di Alta gravità in Docmost, la piattaforma collaborativa open-source per la documentazione.
Il sito ufficiale di Docmost la presenta come un wiki aziendale on-premise 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 risiedeva nel percorso generico di upload dei file che Docmost utilizza anche per i flussi di salvataggio/aggiornamento dei diagrammi.
Stavo esaminando quel codice con una domanda molto specifica in mente:
Cosa succede se l'endpoint di upload verifica l'accesso in modifica su una pagina, ma il target di sovrascrittura viene selezionato con un ID allegato controllato dall'utente separato?
In questo caso, quella domanda ha portato direttamente a un reale fallimento del binding degli oggetti.
Docmost permetteva a un chiamante di inviare:
pageId per una pagina a cui era consentito modificare, eattachmentId appartenente a una pagina diversa nello stesso spazio di lavoroIl server eseguiva un controllo di consistenza della sovrascrittura, ma la guardia utilizzava la logica booleana sbagliata.
Ciò significava che la richiesta poteva superare l'autorizzazione e sovrascrivere comunque l'allegato vittima.
Questo problema è diventato CVE-2026-34213.
Docmost: docmost/docmost
Avviso: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
Corretto in: v0.71.0
---
pageId controllato dall'attaccante con accesso in modifica -> attachmentId vittima controllato dall'attaccante -> guardia di sovrascrittura difettosa considera valida la sovrascrittura tra pagine -> percorso di archiviazione ricostruito dall'attaccanteId vittima -> byte dell'attaccante sostituiscono il file vittima -> la pagina vittima continua a servire l'allegato modificato
Docmost memorizza gli allegati delle pagine caricati come record di database più file di backup nell'archiviazione.
Per i caricamenti normali, il server crea un nuovo ID allegato e scrive un nuovo file.
Per i flussi di salvataggio/aggiornamento dei diagrammi, invece, il client riutilizza intenzionalmente un attachmentId esistente in modo che lo stesso file di diagramma possa essere aggiornato sul posto invece di generare un nuovo record di allegato ogni volta.
Questo comportamento è legittimo di per sé.
Il problema è che crea un percorso ad alto rischio:
Ogni volta che un endpoint combina queste due responsabilità, l'implementazione deve vincolarle esattamente tra loro.
Docmost non lo ha fatto.
Gli endpoint misti di creazione/aggiornamento sono luoghi comuni per i bug di autorizzazione.
Il motivo è semplice:
Questo è esattamente lo schema qui.
POST /api/files/upload convalidava che il chiamante potesse modificare la pagina denominata da pageId.
Ma se veniva fornito anche attachmentId, il server passava a un percorso di sovrascrittura e selezionava un record di allegato esistente separatamente.
Questo rendeva la domanda di sicurezza critica:
il percorso di sovrascrittura dimostra che l'allegato selezionato appartiene effettivamente alla pagina autorizzata?
La risposta nelle versioni vulnerabili era no.
La causa principale era un bypass dell'autorizzazione tramite una chiave controllata dall'utente, combinato con un bug di logica booleana nella guardia di sovrascrittura.
Il flusso vulnerabile appariva così:
AttachmentController.uploadFile() leggeva pageId dai dati del modulo multipart.validateCanEdit(page, user).attachmentId opzionale dalla stessa richiesta.AttachmentService.uploadFile() caricava l'allegato esistente tramite quell'ID fornito dall'attaccante.&& invece di rifiutare in caso di mancata corrispondenza.La guardia vulnerabile era:
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Quella condizione rifiutava la richiesta solo se:
tutti contemporaneamente.
Questo è l'opposto di ciò che una guardia di sovrascrittura dovrebbe fare.
Nel caso reale dell'attacco, l'attaccante rimaneva intenzionalmente all'interno dello stesso spazio di lavoro.
Quindi:
existingAttachment.workspaceId !== workspaceId era falseUna volta che quell'operando diventava falso, l'intera condizione && veniva valutata come falsa, anche se l'allegato apparteneva a una pagina diversa.
Quindi il server trattava una sovrascrittura tra pagine come valida.
Questa era la prima metà del bug.
La seconda metà è ciò che ha reso reale l'impatto.
Dopo il controllo, il servizio ricostruiva il percorso di destinazione dell'archiviazione usando l'attachmentId e il nome file forniti dall'attaccante:
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
Poi, sul percorso di aggiornamento, Docmost aggiornava solo i metadati mutabili come:
fileSizeupdatedAtNon riassociava la proprietà alla pagina dell'attaccante.
Quindi la pagina vittima continuava a puntare allo stesso record di allegato e allo stesso ID allegato. Solo i byte del file sottostante cambiavano.
Ecco perché non si trattava di una mancata corrispondenza innocua.
Era una primitiva di sovrascrittura non autorizzata persistente.
Non era un bug estetico né un problema di collisione di nomi file.
L'attaccante non aveva bisogno di una race condition. L'attaccante non doveva indovinare un percorso casuale. L'attaccante non aveva bisogno di accesso in scrittura alla pagina vittima.
Aveva bisogno solo di:
Da lì, poteva sostituire i byte del file memorizzato per l'allegato di un'altra pagina mentre la pagina vittima continuava a fare riferimento e servire quell'allegato come se nulla fosse cambiato.
Questo è un fallimento diretto dell'integrità.
In termini pratici, l'attaccante poteva:
Il punto importante è questo:
il server accettava un target di sovrascrittura scelto dall'attaccante, senza vincolarlo alla pagina di cui era stato effettivamente verificato il permesso di modifica.
Questo è un fallimento del controllo degli accessi, non solo una cattiva igiene booleana.
Lo sfruttamento era particolarmente pratico per gli allegati dei diagrammi.
Il client di Docmost riutilizza intenzionalmente attachmentId per i salvataggi dei diagrammi e utilizza nomi file deterministici:
diagram.excalidraw.svgdiagram.drawio.svgQuesto è importante perché abbassa i requisiti dell'attaccante.
Per gli allegati generici, l'attaccante ha bisogno sia di:
Per i diagrammi, il nome file è già prevedibile.
Quindi, se l'attaccante può leggere il contenuto della pagina vittima, spesso può recuperare l'unico pezzo mancante di cui ha bisogno:
attachmentId vittimaNella mia configurazione di validazione, ho usato esattamente quel percorso:
Questo è bastato.
Lo sfruttamento ha attraversato i confini delle pagine e dei confini degli spazi all'interno dello stesso spazio di lavoro, soddisfacendo comunque il controllo difettoso dello spazio di lavoro.
Ho convalidato il problema in tempo reale contro Docmost v0.70.3 usando un laboratorio usa e getta costruito da docmost/docmost:0.70.3, Postgres e Redis.
Il flusso del PoC era:
attachmentId vittima.POST /api/files/upload con:
pageId = ID pagina attaccanteattachmentId = ID allegato vittimafile = file sostitutivo controllato dall'attaccante usando il nome file vittimaLa forma minima della richiesta era:
POST /api/files/upload
Content-Type: multipart/form-data
pageId=<attackerPageId>
attachmentId=<victimAttachmentId>
[email protected];filename=diagram.excalidraw.svg
Il risultato live osservato era:
019d18ae-b176-751c-8525-b5f3cede131d019d18ae-b15b-70e9-ac67-64948e87cc5e019d18ae-b12f-75ec-8c1c-5aff3ba6be9c200 OK686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
Sostituzione attaccante da un'altra pagina
Questa è una prova completa di sovrascrittura end-to-end, non solo una revisione teorica del codice sorgente.
Ho usato due stili di prova durante il triage:
L'harness autonomo era utile per isolare il fallimento della logica booleana.
Il PoC HTTP live era l'artefatto più forte perché dimostrava l'intera storia di sicurezza:
attachmentId vittima è stato accettatoQuesta distinzione è importante nei bug di controllo degli accessi.
"la condizione è sbagliata" non è sufficiente da sola.
"la condizione è sbagliata e l'applicazione può essere guidata end-to-end in una sovrascrittura non autorizzata persistente" è il caso completo.
La correzione è stata distribuita in v0.71.0 e ha cambiato la guardia di sovrascrittura da && a ||:
if (
existingAttachment.pageId !== pageId ||
existingAttachment.fileExt !== preparedFile.fileExtension ||
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Quella patch è minima, diretta e corretta per il bug che è stato segnalato.
Ripristina la regola giusta:
la sovrascrittura è consentita solo quando l'allegato esistente corrisponde esattamente ai presupposti di pagina/spazio di lavoro/tipo autorizzati.
Una volta che la guardia rifiuta su qualsiasi mancata corrispondenza:
Questa era la giusta tipologia di correzione:
Solo un binding stretto tra la pagina autorizzata e il target di sovrascrittura.
C'è ancora una lezione di ingegneria più ampia qui:
gli endpoint di upload generici che servono anche flussi di aggiornamento in-place dovrebbero essere trattati come superfici API ad alto rischio.
Anche quando il bug immediato è risolto, progetti più forti a lungo termine sono:
Ma per la vulnerabilità stessa, la patch pubblicata ha chiuso il problema principale in modo pulito.
Che il progetto abbia aggiunto o meno i propri test privati intorno alla correzione, questi sono i casi che contano per la copertura a lungo termine:
Il punto di questi test non è solo la correttezza.
È bloccare il binding di autorizzazione in modo che futuri refactoring "utili" dell'upload non riaprano la stessa classe di bug.
L'avviso pubblicato ha classificato questo problema come:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Ciò si colloca a 7.1 / Alta, che è la conclusione giusta.
La metrica importante qui è l'Integrità.
Non era un bug di metadati di basso livello. L'attaccante controllava completamente i byte di sostituzione scritti nel percorso dell'allegato di un'altra pagina, e la pagina vittima continuava a servire l'oggetto modificato successivamente.
Questo è esattamente il tipo di manomissione incrociata dei record memorizzati che merita Integrità Alta.
La Disponibilità rimane Bassa ha senso anche perché corrompere un diagramma o un documento allegato può rendere inutilizzabile il contenuto vittima, ma l'impatto principale è ancora la modifica non autorizzata piuttosto che l'interruzione completa del servizio.
Ho segnalato il problema privatamente tramite GitHub Security Advisories con:
Il problema è stato accettato dal manutentore, è stato assegnato CVE-2026-34213 e pubblicato il 14 aprile 2026.
L'avviso pubblico elenca:
>= v0.3.0v0.71.0Questa cronologia corrispondeva anche alla mia revisione locale del codice sorgente: la logica di sovrascrittura vulnerabile era presente nella prima release taggata che ho controllato nella riga vulnerabile.
La lezione interessante qui non è semplicemente "usa || invece di &&."
Questo è il sintomo.
La lezione più profonda è:
se un campo controllato dall'utente dimostra l'autorizzazione e un altro campo controllato dall'utente seleziona l'oggetto da aggiornare, questi due campi devono essere vincolati insieme esplicitamente ed esattamente.
Questa regola si presenta ovunque:
Nel momento in cui un sistema dice:
ha creato un confine di sicurezza che deve essere applicato con invarianti di corrispondenza esatta.
Qualunque cosa più morbida di questo si trasforma prima o poi in un bug di chiave controllata dall'utente.
Questo problema rafforza anche un secondo punto che è facile sottovalutare:
piccoli errori booleani nel codice delle guardie possono avere conseguenze di sicurezza di primo ordine.
Una condizione a tre clausole che "sembra ragionevole" a prima vista è bastata per invertire il modello di protezione per il percorso di sovrascrittura.
Ecco perché queste superfici meritano una revisione deliberata piuttosto che una fiducia casuale.
pageId fornito dal chiamante, ma la selezione del target di sovrascrittura utilizzava un attachmentId separato fornito dal chiamante.v0.71.0 ha correttamente modificato la guardia per rifiutare su qualsiasi mancata corrispondenza.Questa vulnerabilità non riguardava un comportamento di archiviazione esotico.
Riguardava un percorso di aggiornamento che si fidava di un identificatore di oggetto selezionato dall'attaccante più di quanto avrebbe dovuto.
Docmost dimostrava l'accesso in modifica su una pagina, accettava un ID allegato esistente da un'altra pagina, e poi lasciava che un controllo di sovrascrittura difettoso trasformasse quella mancata corrispondenza in una sostituzione di file tra pagine riuscita.
Ecco perché è diventato CVE-2026-34213.
La patch in v0.71.0 ha risolto il problema immediato in modo pulito, ma la lezione più ampia rimane preziosa:
quando l'autorizzazione e la selezione dell'oggetto sono divise tra campi controllati dall'utente separati, il binding esatto è la proprietà di sicurezza.