# Une extension d’import CSV vulnérable au path traversal via un nom de fichier

> Une extension d'import de contenu ne filtrait pas les caractères ../ dans le nom du fichier uploadé, permettant d'écrire hors du dossier prévu. Anatomie de la faille et correctif.

- Auteur : Clément Hadrot
- Publié le : 2021-11-19
- Mis à jour le : 2021-11-19
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/extension-import-csv-path-traversal-nom-fichier/

## L’essentiel

- Un nom de fichier n'est jamais un chemin de confiance
- ../ suffit à sortir du dossier prévu
- basename et un chemin canonique referment la faille

Dans le cadre d'un audit préalable au déploiement d'une extension d'import de catalogue développée en interne par un client (import de fiches produits via un fichier CSV déposé par un utilisateur autorisé), notre revue de code s'arrête sur une fonction de traitement de fichier apparemment anodine. L'extension permet de nommer librement le fichier importé, dans un souci d'organisation côté utilisateur, en reprenant tel quel le nom fourni pour construire le chemin de destination sur le serveur.

Le code, à première vue fonctionnel et testé en conditions normales, contient une faille de path traversal : en manipulant le nom du fichier envoyé (pas son contenu, seulement son nom), un attaquant authentifié avec un accès limité au formulaire d'import peut écrire un fichier en dehors du dossier prévu, potentiellement jusqu'à l'atteindre un emplacement exécutable par PHP.

## Le code fautif et pourquoi il fonctionne « normalement »

```
// Fonction fautive de l'extension d'import
function moncpt_traiter_import_csv() {
    $fichier   = $_FILES['catalogue'];
    $dossier   = WP_CONTENT_DIR . '/imports/';
    $nom_final = $_POST['nom_fichier']; // fourni par l'utilisateur, non filtré

    $destination = $dossier . $nom_final . '.csv';

    move_uploaded_file( $fichier['tmp_name'], $destination );
}
```

En usage normal, un utilisateur saisit un nom comme `catalogue-hiver-2021`, et le fichier atterrit bien dans `wp-content/imports/catalogue-hiver-2021.csv`, exactement comme prévu. Le test manuel classique — remplir le formulaire, vérifier que le fichier apparaît au bon endroit — ne révèle jamais le problème, puisqu'il ne teste jamais une entrée volontairement malveillante.

## La charge utile qui sort du dossier prévu

> L'essentiel à retenir : Un nom de fichier n'est jamais un chemin de confiance ; ../ suffit à sortir du dossier prévu ; basename et un chemin canonique referment la faille

La concaténation directe de `$dossier` et de `$nom_final` ne filtre aucun caractère spécial. Un utilisateur (ou un attaquant disposant d'un accès suffisant au formulaire, par exemple via un compte compromis à faible privilège) peut soumettre comme valeur de `nom_fichier` :

```
../../../wp-content/plugins/extension-vulnerable/webshell
```

Une fois concaténée, la destination devient :

```
wp-content/imports/../../../wp-content/plugins/extension-vulnerable/webshell.csv
```

Chaque séquence `../` remonte d'un niveau dans l'arborescence avant de redescendre ailleurs. Sur ce chemin précis, trois remontées suffisent à sortir du dossier `imports` et à atteindre le dossier des extensions. Le fichier final porte l'extension `.csv`, ajoutée par le code lui-même, ce qui limite en apparence les dégâts — sauf que si le nom soumis contient déjà un point final avant l'extension attendue et que le serveur interprète certaines doubles extensions, ou si une autre partie du code permet ensuite de renommer ou d'inclure ce fichier, la combinaison devient dangereuse. Dans tous les cas, la capacité même d'écrire un fichier arbitraire en dehors du dossier prévu constitue déjà une faille sérieuse, indépendamment de l'extension finale.

## Le correctif : basename et chemin canonique vérifié

Deux protections complémentaires referment cette faille, et doivent être appliquées ensemble plutôt qu'isolément :

```
function moncpt_traiter_import_csv_corrige() {
    $fichier = $_FILES['catalogue'];
    $dossier = WP_CONTENT_DIR . '/imports/';

    // 1. basename() retire tout élément de chemin, ne garde que le nom final.
    //    "../../../etc/passwd" devient simplement "passwd".
    $nom_brut  = isset( $_POST['nom_fichier'] ) ? (string) $_POST['nom_fichier'] : 'import';
    $nom_sain  = sanitize_file_name( basename( $nom_brut ) );

    if ( empty( $nom_sain ) ) {
        wp_die( esc_html__( 'Nom de fichier invalide.', 'moncpt' ) );
    }

    $destination = $dossier . $nom_sain . '.csv';

    // 2. Vérification canonique : le chemin réel résolu doit rester
    //    strictement à l'intérieur du dossier attendu.
    $dossier_reel      = realpath( $dossier );
    $destination_reelle = realpath( dirname( $destination ) );

    if ( false === $destination_reelle || 0 !== strpos( $destination_reelle, $dossier_reel ) ) {
        wp_die( esc_html__( 'Chemin de destination non autorisé.', 'moncpt' ) );
    }

    move_uploaded_file( $fichier['tmp_name'], $destination );
}
```

`basename()` élimine à lui seul l'essentiel de la faille en ne conservant que le dernier segment du chemin fourni, quels que soient les `../` qui le précèdent. `sanitize_file_name()`, fonction native de WordPress, complète en retirant les caractères spéciaux et les espaces problématiques du nom restant. La vérification par `realpath()` ajoute une seconde barrière indépendante : elle résout le chemin réel après résolution des liens symboliques et des séquences relatives, puis vérifie qu'il commence bien par le chemin du dossier autorisé. Même si un contournement de `basename()` était découvert plus tard (encodage inhabituel, caractères Unicode équivalents), cette seconde vérification resterait une garde-fou indépendante.

## Pourquoi les deux protections, pas une seule

Se fier uniquement à `basename()` laisse un risque résiduel si une future modification du code réintroduit une concaténation de chemin ailleurs sans repasser par cette fonction. Se fier uniquement à la vérification `realpath()` sans nettoyer le nom en amont laisse des caractères indésirables dans les noms de fichiers stockés, source de bugs applicatifs même sans faille de sécurité. Les deux mesures répondent à des couches différentes du problème et se renforcent mutuellement, un principe de défense en profondeur qui s'applique à toute manipulation de nom de fichier fourni par un utilisateur, quelle que soit l'extension WordPress concernée.

> Sur ce type d'audit, notre test systématique consiste à soumettre `../../../../tmp/test-traversal` dans tout champ qui influence un nom de fichier ou un chemin, avant même de lire le reste du code : si le fichier de test apparaît hors du dossier attendu, la faille est confirmée en quelques secondes.

## Ce que ce cas ne couvre pas

L'inclusion de fichiers PHP via une fonction comme `include()` ou `require()` alimentée par une entrée utilisateur constitue une famille de vulnérabilités distincte, avec ses propres vecteurs et corrections, non traitée ici. Le principe reste néanmoins transposable : jamais de confiance dans un chemin ou un nom de fichier fourni de l'extérieur, sans validation stricte et vérification du résultat final.
