Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2026-34212 — Docmost a accepté une URL javascript: à l'intérieur d'un attachment node, l'a préservée lors du stockage et du rendu, et l'a retransformée en une ancre cliquable dans l'origine de Docmost. | Kitploit
Outils/GitHubGitHub/0xmrma/cve-2026-34212
Analyse des VulnérabilitésExploitationSécurité WebTests d'IntrusionArticles et RechercheApprentissage et Éducation
GitHub0xmrma/cve-2026-34212

CVE-2026-34212

Docmost a accepté une URL javascript: à l'intérieur d'un attachment node, l'a préservée lors du stockage et du rendu, et l'a retransformée en une ancre cliquable dans l'origine de Docmost.

Voir le dépôt
1il y a 2 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-34212

Docmost a accepté une URL de type javascript: dans un nœud de pièce jointe, l’a conservée lors du stockage et du rendu, et l’a transformée en un lien cliquable dans l’origine de Docmost.

Introduction

J’ai identifié, divulgué de manière responsable et reproduit un problème de stored XSS de sévérité élevée dans Docmost, la plateforme de documentation collaborative open-source.

Le site officiel de Docmost le présente comme un wiki interne prêt pour l’entreprise avec plus de 3 millions de téléchargements, et indique qu’il est utilisé par des équipes d’organisations telles que Vilnius City, Bechtle, le gouvernement australien, la Croix-Rouge et ETS Québec.

Le bogue se trouvait à un endroit facile à manquer dans les systèmes de texte enrichi :

pas dans l’extension de lien ordinaire, mais dans un type de nœud personnalisé distinct utilisé pour les pièces jointes.

Je passais en revue le pipeline de l’éditeur avec une question très précise en tête :

si les liens normaux bloquent les URLs javascript:, les nœuds de pièces jointes appliquent-ils la même règle avant d’atteindre un sink d’ancrage ?

Dans les versions vulnérables, ce n’était pas le cas.

Docmost acceptait un nœud de pièce jointe malveillant dans le JSON de la page, stockait son attribut url sans modification, puis rendait cette valeur sous la forme d’un élément cliquable.

<a href="javascript:...">

Ce problème est devenu CVE-2026-34212.

Docmost : docmost/docmost
Avis : GHSA-cf68-cff9-hq4w
CVE : CVE-2026-34212
Corrigé dans : v0.71.0

photo0

Chaîne d’attaque

URL du nœud de pièce jointe contrôlée par l’attaquant -> JSON de la page accepté et stocké sans modification -> rendu HTML/React transformant cette URL en href d’ancrage -> victime clique sur l’action de la pièce jointe -> JavaScript contrôlé par l’attaquant s’exécute dans l’origine de Docmost


Ce que fait cette partie de Docmost

Docmost stocke le contenu des pages dans un format JSON compatible avec ProseMirror/Tiptap.

Ce modèle de contenu inclut des nœuds de bloc personnalisés pour des éléments tels que :

  • images
  • diagrammes
  • intégrations
  • pièces jointes

Le nœud de pièce jointe stocke des champs comme :

  • url
  • name
  • mime
  • size
  • attachmentId

Le serveur accepte le contenu des pages dans plusieurs formats :

  • json
  • markdown
  • html

et le normalise en JSON ProseMirror avant de le stocker.

Cela signifie que tout type de nœud pouvant porter une URL fait partie d’une frontière de confiance directe.

Si l’un de ces types de nœuds finit par être rendu sous la forme d’un <a href>, la gestion des schémas d’URL n’est pas facultative. Elle fait partie du modèle de sécurité.


Pourquoi cette surface méritait d’être examinée

Les extensions d’éditeur personnalisées sont une source fréquente de dérive de sécurité.

Le système de base peut déjà savoir comment traiter correctement les URLs dangereuses, mais chaque nœud personnalisé doit réappliquer les mêmes règles à ses propres sinks.

Cela crée une stratégie de révision prévisible :

  • trouver chaque type de nœud qui stocke un champ de type URL
  • tracer où ce champ est accepté
  • tracer où ce champ est rendu
  • comparer son comportement de nettoyage avec celui du traitement normal des liens de la plateforme

C’est exactement ce qui a exposé ce bogue.

L’extension de lien normal de Docmost traitait déjà javascript: comme dangereux.

Son nœud de pièce jointe ne le faisait pas.

Une fois que vous voyez cette asymétrie, la question de sécurité devient évidente :

puis-je persister un nœud de pièce jointe dont l’url est javascript: et obtenir qu’il soit rendu sous forme d’ancrage actif ?

La réponse était oui.


Cause racine

La cause racine était un nettoyage d’URL incohérent entre les types de nœuds de contenu.

Le chemin de contenu côté serveur acceptait des URLs de pièces jointes arbitraires tant que le contenu global correspondait au schéma ProseMirror.

Dans la version vulnérable :

  • CreatePageDto acceptait content?: string | object
  • PageService.parseProsemirrorContent() normalisait markdown, html ou json
  • le serveur appelait ensuite jsonToNode(prosemirrorJson)
  • si la validation du schéma réussissait, le contenu était stocké

Cette étape de validation vérifiait la validité structurelle, pas la sécurité de l’URL.

La partie critique de la logique serveur vulnérable était effectivement :

root@kitploit:~
prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;

Aucune normalisation de schéma d’URL de pièce jointe n’avait lieu ici.

Plus tard, l’extension de pièce jointe rendait directement la valeur contrôlée par l’attaquant.

Le nœud de pièce jointe vulnérable faisait ceci :

root@kitploit:~
url: {
  default: "",
  parseHTML: (element) => element.getAttribute("data-attachment-url"),
  renderHTML: (attributes) => ({
    "data-attachment-url": attributes.url,
  }),
},

puis :

root@kitploit:~
[
  "a",
  {
    href: HTMLAttributes["data-attachment-url"],
    class: "attachment",
    target: "blank",
  },
  `${HTMLAttributes["data-attachment-name"]}`,
]

Côté client, la vue de nœud React encapsulait cela à nouveau dans :

root@kitploit:~
<a href={getFileUrl(url)} target="_blank">

Mais getFileUrl() ne traitait de manière spéciale que :

  • les URLs http absolues
  • /api/...
  • /files/...

Tout le reste était renvoyé sans modification.

Ainsi, une charge utile comme :

root@kitploit:~
javascript:alert(document.domain)

survivait à :

  • stockage JSON
  • validation du schéma côté serveur
  • rendu HTML
  • gestion d’URL côté client

Cela seul serait déjà suffisant pour une XSS stockée.

Ce qui rend la cause racine particulièrement claire, c’est le point de comparaison.

L’extension de lien normal de Docmost bloquait explicitement javascript: :

  • elle rejetait javascript: dans parseHTML()
  • elle effaçait un href javascript: dans renderHTML()

Le produit savait donc déjà que ce schéma était dangereux.

Le nœud de pièce jointe n’a tout simplement pas appliqué la même politique.

Voilà pourquoi ce n’était pas une « XSS générique dans l’éditeur ».

C’était un écart de frontière de confiance propre à un nœud.


Pourquoi c’est un problème de sécurité, pas seulement un manque de nettoyage

Ce bogue ne concernait pas simplement une esthétique HTML dangereuse.

Il permettait à un attaquant capable de modifier une page de persister une charge utile malveillante qui s’exécuterait plus tard dans l’origine de Docmost lorsqu’un autre utilisateur interagirait avec la pièce jointe rendue.

Cela compte car un script dans l’origine peut :

  • lire les données auxquelles la victime a accès
  • envoyer des requêtes authentifiées en tant que victime
  • modifier le contenu que la victime est autorisée à modifier
  • abuser de toute surface DOM ou API exposée à la session

L’exigence d’un clic ne réduit pas cela à un problème trivial.

Le clic fait partie du comportement normal du produit : l’interface utilisateur présente intentionnellement la pièce jointe comme un lien/icône actionnable.

La question de sécurité n’est donc pas « l’attaquant peut-il forcer du JS arbitraire sans aucune interaction ? »

La vraie question est :

l’application stocke-t-elle du contenu porteur de script contrôlé par l’attaquant et le présente-t-elle ensuite à d’autres utilisateurs comme un chemin d’interaction de confiance ?

Dans les versions vulnérables, oui.

C’est une XSS stockée.


Pourquoi l’exploitation était pratique

Le chemin d’exploitation était simple :

  • tout utilisateur disposant des droits d’édition de page pouvait placer la charge utile
  • l’URL malveillante survivait au stockage sans modification
  • la page s’affichait normalement
  • les visiteurs n’avaient besoin que d’un accès standard à la page
  • un seul clic sur l’action de la pièce jointe suffisait pour déclencher l’exécution

Cela rendait également les utilisateurs à privilèges élevés des cibles réalistes.

Si un propriétaire d’espace de travail, un administrateur ou un éditeur largement approuvé consultait un contenu contrôlé par l’attaquant et cliquait sur l’action de la pièce jointe, le script de l’attaquant s’exécutait dans ce contexte de session plus privilégié.

C’est le point pratique important :

l’exigence de privilège de l’attaquant était seulement faible. Le niveau de privilège de la victime déterminait la valeur de la session XSS.


Preuve de concept

J’ai validé le problème en direct contre Docmost v0.70.3.

Le PoC utilisait uniquement des requêtes HTTP normales et les API de pages de l’application.

Le déroulement était :

  1. Se connecter en tant qu’utilisateur pouvant modifier une page.
  2. Créer ou sélectionner une page.
  3. Envoyer POST /api/pages/update avec format: "json" et un nœud de pièce jointe dont l’url est une charge utile javascript:.
  4. Demander la page via POST /api/pages/info.
  5. Confirmer que le JSON stocké contient toujours l’URL malveillante.
  6. Demander la même page au format HTML et confirmer que le serveur renvoie une ancre dont le href est toujours javascript:....
  7. Dans l’interface utilisateur, un visiteur cliquant sur l’action de pièce jointe rendue exécute la charge utile dans l’origine de Docmost.

Le contenu malveillant minimal était :

root@kitploit:~
{
  "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"
}

Le résultat en direct observé lors de mon test était :

  • l’API acceptait le nœud de pièce jointe malveillant sans modification
  • l’ID de page stocké était 019d18cf-4212-70b0-894a-fe20080fb0f1
  • POST /api/pages/info renvoyait le JSON stocké avec :
root@kitploit:~
"url": "javascript:alert(document.domain)"
  • POST /api/pages/info avec format: "html" renvoyait du HTML contenant :
root@kitploit:~
<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>

Cette réponse HTML est la preuve critique.

Je n’avais pas besoin de m’appuyer sur une affirmation vague selon laquelle « un navigateur pourrait faire quelque chose d’intéressant ».

L’application elle-même rendait le sink exécutable exact.

Lorsqu’un utilisateur clique sur ce lien/icône de pièce jointe, le navigateur exécute l’URL javascript: dans l’origine de la page qui l’a créée.


Pourquoi le PoC a été choisi ainsi

Pour une XSS basée sur l’éditeur, les captures d’écran seules sont des preuves faibles.

Elles montrent des symptômes, pas la défaillance de la frontière.

C’est pourquoi j’ai structuré le PoC autour de deux points de vérification explicites :

  1. preuve de stockage
  2. preuve de sink rendu

La preuve de stockage montrait que le serveur acceptait et préservait le schéma dangereux.

La preuve de sink rendu montrait que l’application transformait cette valeur stockée en :

root@kitploit:~
<a href="javascript:...">

Cette distinction est importante.

Si un produit stocke une entrée dangereuse mais la neutralise avant chaque sink, il peut y avoir un écart de durcissement mais pas nécessairement une XSS active.

Si le produit stocke une entrée dangereuse et la rend ensuite dans un sink d’exécution réel, vous avez la chaîne complète de vulnérabilité.

C’est ce qui s’est passé ici.


Analyse du correctif

Le correctif a été livré dans v0.71.0 et a traité le chemin d’exploitation rendu en appliquant un nettoyage d’URL aux URLs des pièces jointes.

L’extension de pièce jointe importe et utilise désormais sanitizeUrl, y compris :

  • nettoyage de data-attachment-url lors du parsing
  • nettoyage de data-attachment-url lors du rendu
  • nettoyage du href de l’ancre

Conceptuellement, le patch a fait passer le nœud de pièce jointe de :

  • faire confiance à l’URL brute de la pièce jointe
  • émettre l’URL brute de la pièce jointe

à :

  • normaliser l’URL de la pièce jointe avant qu’elle ne fasse partie du nœud rendu

L’assistant côté client getFileUrl() a également été mis à jour afin que les schémas inconnus ne passent plus sans modification. Dans la version corrigée, le chemin de repli renvoie sanitizeUrl(src) au lieu de renvoyer src tel quel.

C’est une partie importante du correctif car la conception vulnérable avait deux problèmes qui se renforçaient mutuellement :

  • le nœud rendait un href brut
  • le repli côté client traitait les schémas inconnus comme acceptables

Le patch a supprimé ces deux hypothèses.

C’était un bon correctif pour le chemin XSS actif car il a aligné le traitement des URLs de pièces jointes sur le reste du modèle de sécurité de l’éditeur.

Cela dit, il reste une leçon plus large de durcissement :

le nettoyage côté client ou au moment du rendu est nécessaire ici, mais le rejet côté serveur des schémas dangereux lors de la création/mise à jour de page serait une invariante encore plus forte.

Le modèle le plus sûr à long terme est :

  • rejeter les schémas manifestement dangereux à l’ingestion
  • nettoyer à nouveau aux frontières de rendu

La défense en profondeur est importante dans les systèmes de contenu enrichi.


Cas de régression qui comptent

Pour une couverture à long terme, voici les cas les plus importants :

  • mises à jour JSON de page contenant attachment.attrs.url = "javascript:..."
  • importations HTML contenant data-attachment-url="javascript:..."
  • le rendu des pièces jointes ne doit jamais émettre href="javascript:..."
  • les assistants de repli côté client ne doivent pas renvoyer de schémas exécutables inconnus inchangés
  • les nœuds de pièces jointes et les nœuds de liens normaux doivent partager une politique de schéma d’URL équivalente
  • les chemins de pièces jointes internes sécurisés tels que /api/files/... et /files/... doivent continuer à fonctionner normalement

Le point clé est la cohérence.

Si les liens normaux sont nettoyés mais que les nœuds personnalisés porteurs d’URL ne le sont pas, l’éditeur n’a pas vraiment de politique de sécurité d’URL unique.

Il a des fragments, et les fragments sont l’endroit où vivent les bogues XSS.


Sévérité et classification

L’avis publié a classé ce problème comme :

  • CWE-79 : Neutralisation incorrecte de l’entrée lors de la génération de pages Web
  • CVSS v3.1 :
root@kitploit:~
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N

Cela donne un score de 7.6 / Élevé.

Il s’agit d’une classification défendable.

Les propriétés importantes sont :

  • faible exigence de privilège pour l’attaquant
  • charge utile stockée
  • exécution dans l’origine de Docmost
  • changement de portée
  • impact significatif sur la confidentialité car le script peut accéder aux données visibles par la victime dans l’application

L’interaction de l’utilisateur reste requise car la victime doit activer le lien/icône de la pièce jointe. C’est pourquoi UI:R est correct.

Mais une fois cette interaction réalisée, la frontière de sécurité a déjà échoué bien plus tôt : l’application a stocké un schéma dangereux et l’a rendu dans un sink d’exécution.


Divulgation

J’ai signalé le problème de manière privée via GitHub Security Advisories avec :

  • une analyse de cause racine
  • un PoC HTTP en direct
  • des preuves JSON stockées
  • des preuves de sink HTML rendu
  • un laboratoire de test jetable épinglé

Le problème a été accepté, s’est vu attribuer CVE-2026-34212, et a été publié le 14 avril 2026.

L’avis public liste actuellement :

  • version affectée : 0.70.3
  • version corrigée : 0.71.0

Ma validation en direct a été effectuée sur v0.70.3, qui correspondait à la version vulnérable publiée.


Ce que ce bogue enseigne réellement

La leçon principale ici n’est pas simplement « nettoyez les URLs ».

Tout le monde le sait déjà.

La leçon plus intéressante est :

si une application a un type de nœud porteur d’URL sécurisé et un type de nœud porteur d’URL non sécurisé, c’est le non sécurisé qui constitue la véritable politique.

Les systèmes de texte enrichi accumulent souvent des extensions personnalisées plus vite qu’ils n’accumulent de révisions de sécurité.

Cela crée exactement ce type d’asymétrie :

  • le chemin de lien standard est durci
  • le chemin de pièce jointe est traité comme « interne » ou « spécial »
  • le chemin spécial devient silencieusement le sink XSS le plus facile

Ce bogue montre également pourquoi la validation de schéma ne suffit pas.

jsonToNode() vérifiait que le contenu était structurellement valide selon ProseMirror. Cela ne prouvait pas que le contenu était sûr à rendre.

Ce sont des questions différentes.

La révision de sécurité devient beaucoup plus précise lorsque vous gardez ces questions séparées :

  • ce contenu est-il structurellement valide ?
  • ce contenu est-il sûr à stocker ?
  • ce contenu est-il sûr à rendre à chaque sink ?

Le nœud de pièce jointe a réussi la première question et échoué à la troisième.

C’est ainsi que les bogues de contenu stocké survivent à l’intérieur de pipelines d’éditeurs par ailleurs bien structurés.


Points clés

  • Docmost acceptait les URLs brutes des nœuds de pièces jointes dans le contenu des pages.
  • La validation côté serveur vérifiait la forme du schéma ProseMirror, pas la sécurité du schéma d’URL.
  • Le nœud de pièce jointe vulnérable rendait data-attachment-url et le href de l’ancre directement à partir de l’entrée contrôlée par l’attaquant.
  • L’assistant côté client getFileUrl() renvoyait les schémas inconnus inchangés.
  • Les nœuds de liens normaux bloquaient déjà javascript:, mais les nœuds de pièces jointes ne le faisaient pas.
  • Un éditeur à faibles privilèges pouvait placer la charge utile une fois et cibler les visiteurs ultérieurs.
  • Le PoC en direct a prouvé à la fois la persistance stockée et le sink exécutable rendu.
  • Le correctif dans v0.71.0 a ajouté la gestion sanitizeUrl au nœud de pièce jointe et au chemin de repli côté client.

Derniers mots

Cette vulnérabilité ne concernait pas une bizarrerie de navigateur.

Elle concernait un nœud de contenu personnalisé qui contournait les propres hypothèses de sécurité d’URL de l’application.

Docmost a accepté une URL de pièce jointe contrôlée par l’attaquant, l’a conservée lors du stockage, puis l’a rendue sous forme d’ancre active dans l’origine de l’application.

C’est pourquoi elle est devenue CVE-2026-34212.

Le correctif dans v0.71.0 a fermé proprement le chemin XSS actif, mais la leçon plus large est celle à retenir :

dans les applications centrées sur l’éditeur, chaque nœud personnalisé pouvant porter une URL constitue sa propre frontière de sécurité, et il doit être examiné comme tel.

Télécharger l’outil