# Exposer un chemin d’upload prévisible dans wp_upload_dir sans contrôle d’accès

> Déplacer un fichier sensible hors du webroot public ne suffit pas si son emplacement reste devinable. Le vrai problème est l'absence de contrôle d'accès, pas l'emplacement.

- Auteur : WordPress Développement
- Publié le : 2023-04-04
- Mis à jour le : 2023-04-04
- Catégorie : Sécurité
- URL : https://www.wpmoderne.fr/securite/chemin-upload-previsible-wp-upload-dir/

## L’essentiel

- Un chemin devinable reste exploitable même hors du dossier public
- wp_upload_dir ne fournit aucune protection d'accès par lui-même
- Servir un fichier sensible via une route PHP contrôlée, pas un lien direct

```
$dossier = wp_upload_dir();
$chemin  = $dossier['basedir'] . '/factures/facture-1042.pdf';
```

Ce code, rencontré sur un module de facturation personnalisé, illustre une confusion fréquente : penser que déplacer un fichier sensible dans un sous-dossier du répertoire d'uploads standard, plutôt que de le laisser directement accessible depuis la racine publique, suffit à le protéger. `wp_upload_dir()` renvoie un tableau de chemins (`basedir`, `baseurl`, etc.) utiles pour organiser le stockage de fichiers, mais cette fonction ne configure aucune restriction d'accès : le dossier `uploads` reste, par défaut, servi directement par le serveur web comme n'importe quel autre contenu statique.

## Ce qu'on observe

Le module de facturation en question générait une URL du type `https://exemple.fr/wp-content/uploads/factures/facture-1042.pdf` pour chaque document, en s'appuyant sur un identifiant de commande incrémental. Techniquement, aucun mécanisme de path traversal n'est en jeu ici — le chemin est parfaitement légitime et prévu par le code. Le problème est ailleurs : n'importe qui devinant ou incrémentant l'identifiant dans l'URL accède directement au fichier, sans passer par aucune vérification d'identité ni de propriété du document.

## Pourquoi c'est un problème même « hors du webroot public »

Un argument entendu régulièrement consiste à dire que le dossier `uploads` n'est « pas censé » être navigué directement, ou qu'il est protégé par l'absence de lien visible depuis le site. Cet argument ne tient pas : le dossier reste accessible en lecture directe par HTTP dès lors que son chemin est connu, et rien dans la configuration par défaut d'un hébergement WordPress ne restreint cet accès. La sécurité par l'obscurité — supposer qu'une URL non publiée reste secrète — ne constitue jamais un contrôle d'accès valable, surtout face à un identifiant séquentiel trivialement énumérable.

> L'essentiel à retenir : Un chemin devinable reste exploitable même hors du dossier public ; wp_upload_dir ne fournit aucune protection d'accès par lui-même ; Servir un fichier sensible via une route PHP contrôlée, pas un lien direct

Un attaquant, ou simplement un client curieux, n'a besoin que de faire varier le nombre en fin d'URL pour accéder aux factures d'autres clients, sans avoir besoin d'aucune compétence technique particulière.

## Ce qu'il faut faire à la place

La correction ne consiste pas à déplacer le fichier ailleurs, mais à ne plus le servir directement par une URL statique. Le fichier doit être stocké hors de portée d'une requête HTTP directe — par exemple dans un répertoire protégé par un `.htaccess` qui refuse tout accès direct, ou totalement en dehors du répertoire `uploads` — puis servi via une route PHP qui vérifie l'identité et les droits du demandeur avant de transmettre le contenu :

```
add_action( 'template_redirect', function () {
    if ( ! isset( $_GET['facture'] ) ) {
        return;
    }

    $commande_id = absint( $_GET['facture'] );
    $commande    = get_post( $commande_id );

    if ( ! $commande || ! is_user_logged_in() ) {
        wp_die( __( 'Accès refusé.', 'mon-plugin' ), 403 );
    }

    $proprietaire = (int) get_post_meta( $commande_id, 'client_id', true );
    if ( get_current_user_id() !== $proprietaire && ! current_user_can( 'manage_options' ) ) {
        wp_die( __( 'Accès refusé.', 'mon-plugin' ), 403 );
    }

    $chemin = mon_plugin_chemin_facture( $commande_id );
    if ( ! file_exists( $chemin ) ) {
        wp_die( __( 'Fichier introuvable.', 'mon-plugin' ), 404 );
    }

    header( 'Content-Type: application/pdf' );
    header( 'Content-Disposition: inline; filename="facture.pdf"' );
    readfile( $chemin );
    exit;
} );
```

## Vérifier l'appartenance, pas seulement l'authentification

Un point souvent oublié dans ce type de correction : vérifier que l'utilisateur est connecté ne suffit pas si tous les utilisateurs connectés peuvent accéder à n'importe quel document. La vérification d'appartenance — comparer l'identifiant du demandeur à celui du propriétaire réel du document, comme illustré ci-dessus — reste l'étape qui referme réellement la faille.

## Rendre les identifiants moins triviaux à deviner, en complément

Remplacer un identifiant de commande séquentiel par un jeton aléatoire, généré avec `wp_generate_password( 32, false )` et stocké en métadonnée du document, ajoute une couche de protection supplémentaire contre l'énumération. Cette mesure reste néanmoins secondaire : elle ne remplace jamais le contrôle d'accès explicite décrit plus haut, elle le complète pour compliquer une tentative d'accès par force brute.

- Contrôle d'accès explicite sur chaque téléchargement : indispensable
- Identifiant non séquentiel : recommandé en complément
- Emplacement hors du webroot ou protégé par règle serveur : recommandé en complément

## Ce qu'il faut retenir

Un chemin d'upload prévisible n'est un problème que parce qu'il n'est adossé à aucun contrôle d'accès ; à l'inverse, un contrôle d'accès solide rend l'emplacement du fichier presque secondaire. La priorité, sur tout module qui génère des documents liés à un utilisateur ou une commande, reste de servir ces fichiers via une route qui vérifie explicitement qui a le droit de les consulter, plutôt que de compter sur l'obscurité d'une URL.
