$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.

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.