Seafile SeaDoc : une RCE via un lien de partage public (CVE-2026-129167)
Un jeton de partage SeaDoc anonyme atteint tous les endpoints du convertisseur, où un chemin absolu dépose un paquet Python exécutable, corrigé en 2.0.11/3.0.8.
En bref
- Un chemin absolu dans l'import Confluence de SeaDoc permet au détenteur anonyme d'un lien de partage
.sdocpublic d'écrire un paquet Python dans le chemin de bibliothèque du convertisseur et de l'exécuter. Haiwen a corrigé le serveur le 2 novembre 2026. - SeaDoc 2.0.4 à 2.0.10 et 3.0.0 à 3.0.7 sont concernés. L'instance doit avoir un lien de partage
.sdocpublic ou un wiki publié. - L'enregistrement CVE la note CVSS v4.0 9,2 Critique. Aucune exploitation n'est signalée, et elle n'est pas dans le catalogue Known Exploited Vulnerabilities (KEV) de la CISA au 14 novembre 2026.
- Mettez à niveau vers SeaDoc 2.0.11 ou 3.0.8, ou une version ultérieure. Voir « Comment corriger ? » pour le commit du convertisseur et le contournement.
Suis-je concerné ?
Vous êtes exposé lorsque toutes les conditions suivantes sont réunies :
- Le déploiement exécute SeaDoc 2.0.4 à 2.0.10, ou 3.0.0 à 3.0.7.
- L'instance a un lien de partage
.sdocpublic ou un wiki publié. Le jeton anonyme que la page de partage intègre est l'identifiant utilisé par l'attaque. - Le convertisseur SeaDoc est joignable via le frontal Seafile. Le déploiement Docker Compose officiel l'expose sous
/sdoc-server/converter/. - Le déploiement a été fait avant le 2 novembre 2026, date à laquelle Haiwen indique avoir corrigé le composant
sdoc-serverà code source fermé.
Un déploiement sans lien de partage .sdoc public et sans wiki publié n'expose pas le chemin du jeton anonyme décrit ici. Un utilisateur connecté qui peut obtenir un jeton SeaDoc peut néanmoins atteindre le convertisseur.
Le convertisseur accepte tout jeton SeaDoc valide. Un jeton émis pour un document atteint donc tous les points de terminaison du convertisseur. Traitez la condition préalable comme une condition sur le chemin d'attaque, pas comme un état sûr par défaut.
| Branche | Versions concernées | Version corrigée |
|---|---|---|
| SeaDoc 2.0 | 2.0.4 à 2.0.10 | 2.0.11 |
| SeaDoc 3.0 | 3.0.0 à 3.0.7 | 3.0.8 |
Les images actuelles au moment de la rédaction sont 2.0.13 et 3.0.9. Les deux incluent le correctif.
Vérifiez l'image en cours et les partages publics :
# Image tag of the running SeaDoc container
docker inspect --format '{{.Config.Image}}' <seadoc-container>
# Images present on the host
docker image ls | grep sdoc-server
# Public links, in the Seafile web interface
# Open each library, then Sharing, and review the links that are public
Le tag de l'image est la vérification de version fiable pour le déploiement officiel. Le fichier compose peut épingler un tag comme 2.0-latest ou 3.0-latest, qui peut changer sans que la version du conteneur en cours change.
Comment corriger ?
Mettez à niveau SeaDoc vers 2.0.11 ou 3.0.8, ou vers les versions actuelles 2.0.13 ou 3.0.9, et recréez les conteneurs.
- Récupérez l'image corrigée et mettez à jour le tag dans le fichier compose.
- Recréez les conteneurs
seadocetsdoc-server. Récupérer l'image sans recréer le conteneur laisse l'ancien processus en cours. - Confirmez le nouveau tag avec
docker inspect. - Examinez les liens
.sdocpublics et les wikis publiés. Retirez ceux qui ne sont pas nécessaires. - Si vous ne pouvez pas mettre à niveau, bloquez les points de terminaison du convertisseur au niveau du reverse proxy, ou retirez tous les liens
.sdocpublics et dépubliez tous les wikis. Le contournement supprime le chemin du jeton anonyme ; il ne corrige pas le chemin absolu.
Le correctif du convertisseur open source seadoc-converter est le commit cb43162e du 29 octobre 2026. Il rejette un filename qui n'est pas une chaîne, ne se termine pas par .html.zip, ou contient /, \ ou un octet NUL. Il rejette aussi une space_key vide, . et ...
Le composant sdoc-server est à code source fermé. Haiwen a déclaré au chercheur par courriel le 2 novembre 2026 que SeaDoc 2.0 et 3.0 étaient corrigés. Aucun commit ni avis public ne décrit cette moitié du correctif.
Quelle est la cause racine ?
Le convertisseur acceptait tout JWT SeaDoc après avoir vérifié uniquement sa signature. Il utilisait ensuite un nom de fichier contrôlé par l'attaquant comme chemin absolu pour l'extraction du ZIP. Il conservait les fichiers extraits pendant qu'un envoi restait bloqué.
SeaDoc compte trois processus : seahub pour l'authentification et le rendu des pages, sdoc-server pour l'édition collaborative, et seadoc-converter pour la conversion des documents. Les trois partagent la clé HMAC JWT_PRIVATE_KEY. Lorsqu'un visiteur ouvre un lien de partage .sdoc public, seahub intègre un JWT dans la page pour que l'aperçu anonyme fonctionne.
Le check_auth_token() du convertisseur vérifie la signature avec config.SEADOC_PRIVATE_KEY et renvoie vrai. Il ne vérifie jamais le document pour lequel le jeton a été émis ni la permission qu'il porte :
def check_auth_token(req):
auth = req.headers.get('Authorization', '').split()
if not auth or auth[0].lower() != 'token' or len(auth) != 2:
return False
token = auth[1]
if not token:
return False
private_key = config.SEADOC_PRIVATE_KEY
try:
jwt.decode(token, private_key, algorithms=['HS256'])
except (jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
logger.exception(e)
return False
return True
Le jeton anonyme est en lecture seule, mais le convertisseur ignore le champ permission. Le jeton atteint donc /api/v1/confluence-to-wiki/, qui prend un filename et construit un chemin avec os.path.join :
underscore_index = filename.rfind('_')
if underscore_index != -1:
space_key = filename[underscore_index + 1:-len('.html.zip')]
elif 'Confluence-space-export-' in filename:
wiki_name = filename[:-len('.html.zip')]
space_key = filename[len('Confluence-space-export-'):-len('.html.zip')]
else:
wiki_name = filename[:-len('.html.zip')]
space_key = wiki_name
try:
extract_dir = '/tmp/wiki'
space_dir = os.path.join(extract_dir, space_key)
zip_file_path = os.path.join(extract_dir, filename)
if not os.path.exists(extract_dir):
os.mkdir(extract_dir)
# Deploy on two machines
is_same_machine = True
if not os.path.exists(space_dir):
os.mkdir(space_dir)
is_same_machine = False
response = requests.get(download_url)
with ZipFile(BytesIO(response.content), 'r') as zip_ref:
zip_ref.extractall(space_dir)
Lorsque le deuxième argument de os.path.join commence par une barre oblique, Python ignore le premier argument. Une space_key comme /usr/lib/python3.12/encodings/cp273 rend space_dir et zip_file_path absolus. Le convertisseur crée le répertoire et y décompresse le ZIP de l'attaquant.
Le nettoyage s'exécute dans un bloc finally seulement après le retour de process_zip_file(). Cette fonction envoie le résultat à upload_url sans délai d'attente. Une cible d'envoi qui accepte la connexion et ne répond jamais garde la requête bloquée. Le bloc finally ne s'exécute donc pas, et les fichiers déposés restent sur le disque.
Comment la faille est-elle exploitée ?
- L'attaquant ouvre un lien de partage
.sdocpublic et lit la valeurseadocAccessTokendans le source de la page. Le jeton est un identifiant anonyme valide pour ce document. - L'attaquant exécute un serveur qui renvoie un ZIP contenant un
__init__.py, accepte une connexion sans jamais répondre, et renvoie une erreur HTTP 500 aveccharset=cp273lorsqu'on le lui demande. - L'attaquant envoie une requête à
/sdoc-server/converter/api/v1/confluence-to-wiki/avec unfilenameabsolu comme/usr/lib/python3.12/encodings/cp273aaaaaaaaa, undownload_urlqui sert le ZIP, et unupload_urlqui reste bloqué. Le convertisseur extrait le ZIP dans le paquet Pythonencodings. - L'attaquant envoie une requête à
/sdoc-server/converter/api/v1/file-convert/avec unupload_urlqui renvoie 500 etcharset=cp273. Le convertisseur journalise le corps de la réponse, etResponse.textle décode avec le charset déclaré. - CPython importe paresseusement
encodings.cp273.FileFindervérifie le paquet répertoirecp273/__init__.pyavant le modulecp273.py, donc le fichier déposé s'exécute dans le processus du convertisseur.
L'analyse publique du chercheur contient les deux requêtes et le serveur de l'attaquant. La première requête reste bloquée pendant que les fichiers sont sur le disque ; la seconde déclenche l'import. La commande témoin de l'analyse a écrit uid=0 (root) depuis le conteneur du convertisseur.
Le chemin ne nécessite pas de redémarrage, car l'import se fait à la demande dans le processus en cours. La seule condition préalable est un lien de partage .sdoc public ou un wiki publié qui fournit un jeton anonyme.
Comment se déroule l'attaque ?
Un jeton de partage anonyme passe le contrôle de signature. Un nom de fichier absolu écrit un paquet Python dans le chemin de bibliothèque du convertisseur. Une erreur de charset fait importer et exécuter ce paquet par CPython.
intègre un JWT anonyme"] --> B["Le convertisseur vérifie seulement
la signature du JWT"] B --> C["confluence-to-wiki accepte
le jeton sur tous les points de terminaison"] C --> D["Le filename absolu fait écrire
os.path.join hors de /tmp/wiki"] D --> E["Le ZIP est extrait dans
le paquet Python encodings"] E --> F["L'envoi bloqué empêche
le nettoyage finally"] F --> G["charset=cp273 fait importer
le paquet déposé par CPython"] G --> H["Le code s'exécute dans
le processus du convertisseur"]
Que peut faire un attaquant ?
Un attaquant non authentifié qui détient un jeton de partage public peut exécuter du code dans le processus du convertisseur SeaDoc. Le conteneur officiel exécute ce processus en root.
L'impact comprend :
- La lecture et la modification des documents que le convertisseur peut atteindre, y compris la clé HMAC partagée dans sa configuration.
- L'utilisation de cette clé pour forger des jetons SeaDoc pour d'autres documents et services.
- L'accès au réseau et aux volumes montés auxquels le conteneur du convertisseur peut accéder.
- L'utilisation du convertisseur comme point d'appui dans le déploiement Seafile.
Les conditions font partie de l'impact :
- L'instance doit exposer un lien de partage
.sdocpublic ou un wiki publié. - Le convertisseur doit être joignable depuis le réseau de l'attaquant.
- Le déploiement doit être une version SeaDoc concernée construite avant le 2 novembre 2026.
- L'attaquant n'a besoin ni de compte ni d'interaction de l'utilisateur.
Le chercheur a démontré uid=0 (root) dans le laboratoire Docker Compose officiel. Le code s'exécute dans le conteneur du convertisseur, pas sur le serveur applicatif Seafile par défaut. La capacité d'atteindre l'hôte dépend des montages et du réseau du conteneur.
Pourquoi le correctif fonctionne-t-il ?
Le correctif du convertisseur rejette tout nom de fichier qui pourrait sortir de /tmp/wiki, donc os.path.join ne peut plus produire de chemin d'extraction absolu.
Le commit de correction cb43162e modifie la validation au début de confluence_to_wiki() et ajoute un contrôle après la dérivation de space_key :
- if not filename:
+ if (not isinstance(filename, str)
+ or not filename.endswith('.html.zip')
+ or any(char in filename for char in ('/', '\\', '\x00'))):
return {'error_msg': 'path invalid.'}, 400
...
else:
wiki_name = filename[:-len('.html.zip')]
space_key = wiki_name
+
+ if space_key in ('', '.', '..'):
+ return {'error_msg': 'path invalid.'}, 400
try:
extract_dir = '/tmp/wiki'
space_dir = os.path.join(extract_dir, space_key)
Le contrôle rejette une barre oblique, une barre oblique inverse et un octet NUL dans filename, donc chaque space_key dérivée est un seul composant de chemin. Le contrôle de space_key rejette les trois valeurs qui pointeraient encore hors d'un chemin enfant normal. L'extraction reste sous /tmp/wiki.
Il s'agit d'un correctif partiel dans le convertisseur open source. Le changement du sdoc-server à code source fermé qui empêche un jeton anonyme en lecture seule d'atteindre tous les points de terminaison du convertisseur n'est pas public. Haiwen indique que le composant est corrigé ; le changement de portée du jeton n'est pas visible pour la revue.
Comment vérifier si j'ai été compromis ?
Aucun indicateur de compromission du fournisseur n'a été publié. Les vérifications portent sur les deux requêtes au convertisseur et sur les fichiers déposés dans le chemin de bibliothèque Python.
- Recherchez dans les journaux du reverse proxy et de Seafile des requêtes POST vers
/sdoc-server/converter/api/v1/confluence-to-wiki/avec unfilenamequi commence par/ou contient... - Recherchez des requêtes POST vers
/sdoc-server/converter/api/v1/file-convert/dont l'upload_urlpointe hors de votre infrastructure. - Dans le conteneur du convertisseur, listez le répertoire Python
encodingspour des paquets que vous n'avez pas installés, surtout un répertoire nommé d'après un codec commecp273. - Vérifiez
/tmp/wikipour des répertoires et des fichiers ZIP restants. - Vérifiez le conteneur du convertisseur pour des fichiers inattendus sous
/root,/tmpet le répertoire de l'application.
La preuve de concept du chercheur écrit /root/pwned ; un attaquant peut écrire autre chose. Une requête qui échoue avant l'extraction peut ne laisser aucune trace, et le convertisseur journalise le corps de la réponse d'envoi, pas la requête complète.
Chronologie
- 2026-10-24 : le chercheur publie l'analyse.
- 2026-10-29 : le commit de correction
cb43162eduseadoc-converterest rédigé. - 2026-11-02 : Haiwen indique au chercheur que le composant
sdoc-serverà code source fermé est corrigé. - 2026-11-13 : CVE-2026-129167 est publiée à 17h21 UTC.
FAQ
CVE-2026-129167 est-elle exploitée dans la nature ?
Aucune exploitation n'a été signalée. La CVE n'est pas dans le KEV de la CISA au 14 novembre 2026, et l'analyse du chercheur décrit une reproduction en laboratoire, pas une campagne.
Faut-il un compte pour l'exploiter ?
Non. L'attaquant a besoin d'un lien de partage .sdoc public ou d'un wiki publié, et lit le jeton anonyme intégré à la page de partage. Aucun compte Seafile n'est nécessaire.
Le jeton anonyme limite-t-il l'attaque à un seul document ?
Non. Le convertisseur vérifie seulement la signature du jeton. Les champs permission et document du jeton sont ignorés, donc le jeton anonyme atteint tous les points de terminaison du convertisseur.
Le serveur Seafile est-il vulnérable, ou seulement SeaDoc ?
La faille est dans le convertisseur SeaDoc et dans le contrôle de jeton partagé par les services SeaDoc. Un déploiement Seafile sans SeaDoc n'est pas concerné par cette CVE. Le conteneur du convertisseur est le processus qui exécute le code de l'attaquant.
Quelle version corrige la faille ?
SeaDoc 2.0.11 et 3.0.8 sont les premières versions corrigées. Les versions actuelles 2.0.13 et 3.0.9 incluent aussi le correctif. Le composant sdoc-server à code source fermé a été corrigé le 2 novembre 2026.
Le correctif est-il complet ?
Le correctif du convertisseur open source ferme le chemin absolu. Le changement de portée du jeton dans le sdoc-server à code source fermé n'est pas public, donc son exhaustivité ne peut pas être vérifiée à partir du code.
Références
- Enregistrement CVE-2026-129167, CNA VulnCheck, publié le 2026-11-13, CVSS v4.0 9.2 : https://www.cve.org/CVERecord?id=CVE-2026-129167 (vérifié le 14 novembre 2026)
- Wu Yulun, « Seafile Vulnerability Report », publié le 2026-10-24, la cause racine, la reproduction et les deux requêtes : https://vul.yulun.ac.cn/20261024/seafile/ (vérifié le 14 novembre 2026)
- Commit de correction
cb43162eduseadoc-converter, 29 octobre 2026, la validation du nom de fichier et despace_key: https://github.com/haiwen/seadoc-converter/commit/cb43162e8bc2991a2900c8acf85090358a30e516 (vérifié le 14 novembre 2026) - Source vulnérable du
seadoc-converteràbb6cf0f,seadoc_converter/server/apis.py,check_auth_token()etconfluence_to_wiki(): https://github.com/haiwen/seadoc-converter/blob/bb6cf0fb9ebc6bb05c9161acbfd6289737ec2cd6/seadoc_converter/server/apis.py (vérifié le 14 novembre 2026) - Source vulnérable du
seadoc-converteràbb6cf0f,seadoc_converter/converter/utils.py, l'envoi sans délai d'attente : https://github.com/haiwen/seadoc-converter/blob/bb6cf0fb9ebc6bb05c9161acbfd6289737ec2cd6/seadoc_converter/converter/utils.py (vérifié le 14 novembre 2026) - Docker Hub, tags de
seafileltd/sdoc-server, 2.0.11, 3.0.8, 2.0.13 et 3.0.9 : https://hub.docker.com/r/seafileltd/sdoc-server/tags (vérifié le 14 novembre 2026) - Docker Hub, nombre de téléchargements de
seafileltd/seafile-mc, 20 336 673 : https://hub.docker.com/r/seafileltd/seafile-mc (vérifié le 14 novembre 2026) - Catalogue Known Exploited Vulnerabilities de la CISA, vérifié pour CVE-2026-129167 le 14 novembre 2026 (non listée) : https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (vérifié le 14 novembre 2026)