Chemins autorisés pour les liens symboliques

Par défaut, OxPHP refuse toute requête qui se résout vers un chemin situé hors du DOCUMENT_ROOT canonique. Un lien symbolique à l'intérieur du DOCUMENT_ROOT pointant vers un répertoire externe renvoie 404 et la résolution de chemin journalise Blocked request: resolved path escapes document root.

C'est le bon comportement par défaut : il empêche la traversée de répertoires, les attaques TOCTOU par échange de lien symbolique et l'exposition accidentelle de fichiers de configuration ou de secrets situés un niveau au-dessus. Mais il bloque aussi des schémas légitimes que les frameworks utilisent depuis des années : le php artisan storage:link de Laravel, les bundles d'actifs de Symfony, les volumes de téléversement partagés montés dans plusieurs conteneurs.

SYMLINK_ALLOW_PATHS est l'activation explicite : vous y listez les chemins du système de fichiers vers lesquels les liens symboliques sous DOCUMENT_ROOT ont le droit de se résoudre. Tout ce qui ne figure pas dans la liste conserve le comportement strict du 404.

Configuration

bash
# Absolute paths, comma-separated SYMLINK_ALLOW_PATHS=/var/www/storage,/opt/shared/assets # Relative paths resolve against DOCUMENT_ROOT SYMLINK_ALLOW_PATHS=../storage,../shared/uploads # Mixed SYMLINK_ALLOW_PATHS=/opt/shared/cdn,../storage/app/public

Lorsque la variable n'est pas définie (le cas par défaut), aucun lien symbolique ne peut quitter le DOCUMENT_ROOT.

Exemple Laravel

bash
DOCUMENT_ROOT=/app/public SYMLINK_ALLOW_PATHS=../storage/app/public

php artisan storage:link crée alors public/storage -> ../storage/app/public à l'intérieur du projet. Les URL vers /storage/<file> se résolvent à travers le lien symbolique vers /app/storage/app/public/<file>, qui se canonicalise en un chemin autorisé par la liste d'autorisation. Aucune modification de code dans l'application.

Fonctionnement

Au démarrage, chaque entrée est résolue :

  • Entrées absolues — vérifiées par rapport à la liste noire (voir ci-dessous), puis passées à realpath(3) (c'est-à-dire std::fs::canonicalize). Si realpath échoue (la cible n'existe pas), le serveur refuse de démarrer.
  • Entrées relatives — jointes au DOCUMENT_ROOT canonique, puis passées à realpath.

Les chemins canoniques résultants sont stockés comme liste d'autorisation. Les doublons sont dédupliqués silencieusement.

Au moment de la requête, la couche de routage canonicalise le chemin de fichier résolu et vérifie qu'il satisfait l'une des conditions suivantes :

  1. il se trouve à l'intérieur du DOCUMENT_ROOT, ou
  2. il correspond exactement à l'une des entrées de la liste d'autorisation (cibles de type fichier), ou
  3. il commence par l'une des entrées de la liste d'autorisation suivie d'un / (cibles de type répertoire).

Le même contrôle s'exécute une seconde fois comme garde TOCTOU dans le chemin de service des fichiers statiques, après le cache de routes, avant tout appel système de lecture.

Liste noire

Un petit ensemble de chemins ne peut jamais figurer dans SYMLINK_ALLOW_PATHS ; des fautes de frappe et des malentendus élargiraient sinon considérablement la surface d'attaque. Le serveur refuse de démarrer si une entrée se résout vers un chemin de la liste noire.

Interdits en correspondance exacte :

text
/ /etc /proc /sys /dev /var /home /tmp /root /usr /srv

Interdits en préfixe (l'entrée se trouve sous l'un de ces répertoires) :

text
/etc /proc /sys /dev /tmp /root /usr
Note

/var, /home et /srv sont en correspondance exacte uniquement : /srv seul est rejeté, mais /srv/myapp/storage est autorisé, tout comme /var/www/storage et /home/<any>/... le sont. Les entrées sont vérifiées deux fois : une fois par rapport au chemin brut fourni par l'administrateur (afin qu'un /etc -> /private/etc à la macOS ne puisse pas blanchir un chemin de la liste noire à travers realpath), une fois par rapport à la forme canonique (défense en profondeur contre les évasions de cible de lien symbolique).

La liste noire elle-même est codée en dur ; aucune variable d'environnement ne permet de l'étendre. Le comportement par défaut est le minimum conservateur qui attrape les erreurs de type faute de frappe ; les administrateurs qui ont besoin de politiques plus strictes doivent les superposer à l'extérieur (permissions du système de fichiers, restrictions de montage de conteneur, profils AppArmor/SELinux).

Modes de défaillance

Erreur de configuration Résultat
La cible de l'entrée n'existe pas sur le disque Le serveur refuse de démarrer, l'erreur nomme l'entrée et canonicalize
L'entrée correspond à la liste noire (brute ou canonique) Le serveur refuse de démarrer, l'erreur nomme l'entrée et la règle de la liste noire
Variable d'environnement vide ou composée uniquement d'espaces Traitée comme non définie — comportement strict par défaut
Entrées en double Dédupliquées silencieusement après canonicalisation
Aucun lien symbolique n'existe encore au démarrage La liste d'autorisation est enregistrée mais reste inerte jusqu'à l'apparition d'un lien symbolique ; aucun contrôle de démarrage n'exige le lien symbolique

Notes de sécurité

  • La liste d'autorisation est en opt-in — le comportement par défaut sûr « aucune évasion » est préservé lorsque la variable n'est pas définie
  • Les entrées sont canonicalisées au démarrage, de sorte que les .. et les liens symboliques intermédiaires dans le chemin de l'entrée sont réduits avant stockage
  • La canonicalisation à l'exécution ferme la fenêtre TOCTOU d'échange de lien symbolique — le chemin validé est le chemin qui est lu
  • Les cibles de type fichier correspondent exactement ; les cibles de type répertoire correspondent par préfixe de répertoire. Lister un fichier à /etc/passwd serait de toute façon rejeté par la liste noire, mais plus généralement : lister un fichier unique à /opt/shared/license.key n'accorde pas implicitement l'accès à ses voisins
  • Les résultats de validation de chemin sont mis en cache par URL demandée (un realpath par URL unique jusqu'à éviction), de sorte que le coût à l'exécution est amorti

Voir aussi