
Un utilisateur Docmost à faibles privilèges pourrait fournir un `attachmentId` de victime au point de terminaison de téléchargement générique et écraser la pièce jointe stockée d'une autre page dans le même espace de travail.
Un utilisateur Docmost à faibles privilèges pourrait fournir un attachmentId victime au point de terminaison de téléversement générique et écraser la pièce jointe stockée d'une autre page dans le même espace de travail.
J'ai identifié, divulgué de manière responsable et reproduit un défaut d'autorisation de Haute gravité dans Docmost, la plateforme collaborative de documentation open‑source.
Le site officiel de Docmost la présente comme un wiki sur site prêt pour l'entreprise avec 3M+ téléchargements, et affirme qu'elle est utilisée par des équipes d'organisations dont Vilnius City, Bechtle, le Gouvernement australien, la Croix‑Rouge et ETS Québec.
Le bogue vivait dans le chemin générique de téléversement de fichiers que Docmost utilise également pour les flux de sauvegarde/mise à jour de diagrammes.
J'examinais ce code avec une question très spécifique en tête :
Que se passe‑t‑il si le point de terminaison de téléversement prouve l'accès en modification sur une page, mais que la cible d'écrasement est sélectionnée avec un identifiant de pièce jointe distinct contrôlé par l'utilisateur ?
Dans ce cas, cette question a directement conduit à une véritable défaillance de liaison d'objet.
Docmost permettait à un appelant d'envoyer :
pageId pour une page qu'il était autorisé à modifier, etattachmentId appartenant à une page différente dans le même espace de travailLe serveur effectuait bien un contrôle de cohérence d'écrasement, mais la garde utilisait la mauvaise logique booléenne.
Cela signifiait que la requête pouvait passer l'autorisation et écraser la pièce jointe victime quand même.
Ce problème est devenu CVE-2026-34213.
Docmost : docmost/docmost
Avis : GHSA-89fp-2hch-j9gp
CVE : CVE-2026-34213
Corrigé dans : v0.71.0
---
pageId contrôlé par l'attaquant avec accès en modification -> victim attachmentId contrôlé par l'attaquant -> garde d'écrasement défaillante traite l'écrasement inter‑pages comme valide -> chemin de stockage reconstruit à partir du victim attachmentId -> les octets de l'attaquant remplacent le fichier victime -> la page victime continue de servir la pièce jointe modifiée
Docmost stocke les pièces jointes téléversées sous forme d'enregistrements en base de données plus des fichiers de sauvegarde dans le stockage.
Pour les téléversements normaux, le serveur crée un nouvel identifiant de pièce jointe et écrit un nouveau fichier.
Pour les flux de sauvegarde/mise à jour de diagrammes, cependant, le client réutilise intentionnellement un attachmentId existant afin que le même fichier de diagramme puisse être mis à jour sur place au lieu de générer un tout nouvel enregistrement de pièce jointe à chaque fois.
Ce comportement est légitime en soi.
Le problème est qu'il crée un chemin à haut risque :
Chaque fois qu'un point de terminaison mélange ces deux responsabilités, l'implémentation doit les lier exactement.
Docmost ne l'a pas fait.
Les points de terminaison mixtes création/mise à jour sont des endroits courants pour les bogues d'autorisation.
La raison est simple :
C'est exactement le schéma ici.
POST /api/files/upload validait que l'appelant pouvait modifier la page nommée par pageId.
Mais si attachmentId était également fourni, le serveur basculait vers un chemin d'écrasement et sélectionnait un enregistrement de pièce jointe existant séparément.
Cela posait la question de sécurité critique :
le chemin d'écrasement prouve‑t‑il que la pièce jointe sélectionnée appartient bien à la page autorisée ?
La réponse dans les versions vulnérables était non.
La cause racine était un contournement d'autorisation via une clé contrôlée par l'utilisateur, combiné à un bogue de logique booléenne dans la garde d'écrasement.
Le flux vulnérable ressemblait à ceci :
AttachmentController.uploadFile() lisait pageId à partir des données de formulaire multipart.validateCanEdit(page, user).attachmentId optionnel depuis la même requête.AttachmentService.uploadFile() chargeait la pièce jointe existante via cet identifiant fourni par l'attaquant.&& au lieu de rejeter sur toute non‑correspondance.La garde vulnérable était :
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
Cette condition ne rejetait la requête que si :
tous en même temps.
C'est l'inverse de ce qu'une garde d'écrasement devrait faire.
Dans le cas d'attaque réel, l'attaquant restait intentionnellement dans le même espace de travail.
Donc :
existingAttachment.workspaceId !== workspaceId était falseUne fois que cet opérande devenait false, toute la condition && s'évaluait à false, même si la pièce jointe appartenait à une page différente.
Ainsi, le serveur traitait un écrasement inter‑pages comme valide.
C'était la première moitié du bogue.
La seconde moitié est ce qui rend l'impact réel.
Après la vérification, le service reconstruisait le chemin de stockage de destination en utilisant le attachmentId et le nom de fichier fournis par l'attaquant :
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
Ensuite, sur le chemin de mise à jour, Docmost ne mettait à jour que les métadonnées mutables telles que :
fileSizeupdatedAtIl ne reliait pas la propriété à la page de l'attaquant.
Ainsi, la page victime continuait de pointer vers le même enregistrement de pièce jointe et le même identifiant de pièce jointe. Seuls les octets du fichier sous‑jacent changeaient.
C'est pourquoi ce n'était pas une simple non‑correspondance inoffensive.
C'était une primitive d'écrasement persistant non autorisé.
Ce n'était pas un bogue cosmétique ni un problème de collision de noms de fichiers.
L'attaquant n'avait pas besoin d'une condition de course. L'attaquant n'avait pas besoin de deviner un chemin aléatoire. L'attaquant n'avait pas besoin d'accès en écriture à la page victime.
Il avait seulement besoin :