# Sécuriser les téléversements REST : le champ oublié des extensions headless

> Une route REST personnalisée qui accepte des fichiers doit reproduire les contrôles de wp_handle_upload, ce que beaucoup de développeurs headless négligent. Checklist des vérifications.

- Auteur : Clément Hadrot
- Publié le : 2022-09-25
- Mis à jour le : 2022-09-25
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/securiser-televersements-rest-checklist/

## L’essentiel

- register_rest_route n'applique aucun contrôle d'upload par défaut
- wp_handle_upload regroupe des vérifications qu'il faut reproduire
- Une checklist avant d'ouvrir une route d'upload personnalisée

Un projet headless pour un client media construit son front-end en Next.js et communique avec WordPress exclusivement via l'API REST, y compris pour permettre aux journalistes de téléverser des images depuis l'interface de rédaction externe. Le développeur en charge du point d'entrée REST personnalisé, plus habitué aux frameworks JavaScript qu'à l'écosystème WordPress, a écrit une route fonctionnelle en quelques lignes — sans savoir que WordPress applique normalement, lors d'un upload classique via l'administration, une série de vérifications qu'aucune route REST personnalisée n'hérite automatiquement.

C'est un piège récurrent dans les projets headless : l'équipe front-end maîtrise parfaitement son framework, l'équipe back-end connaît WordPress, mais la frontière entre les deux — une route REST personnalisée qui gère un upload — se retrouve parfois écrite par la personne la moins familière avec les protections que WordPress fournit ailleurs, et qui ignore donc qu'il faut les reproduire explicitement.

## Ce que wp_handle_upload fait, sans qu'on y pense

Quand un fichier est envoyé depuis l'administration WordPress classique (médiathèque, champ d'upload d'un formulaire natif), il transite par `wp_handle_upload()`, une fonction qui regroupe silencieusement plusieurs contrôles : vérification du type MIME réel via `wp_check_filetype_and_ext()`, restriction aux extensions autorisées par le filtre `upload_mimes`, génération d'un nom de fichier unique et assaini, et exécution des hooks de sécurité qu'une extension de protection (antivirus, pare-feu applicatif) aurait pu accrocher sur ce point d'entrée standard.

Une route REST personnalisée qui manipule `$_FILES` directement, sans jamais appeler `wp_handle_upload()`, ne bénéficie d'aucune de ces protections — même si l'extension WordPress installée par ailleurs promet une protection générale des uploads, puisque cette protection est souvent accrochée précisément sur ce point d'entrée standard qui, ici, n'est jamais sollicité.

## La checklist des quatre contrôles à reproduire

> L'essentiel à retenir : register_rest_route n'applique aucun contrôle d'upload par défaut ; wp_handle_upload regroupe des vérifications qu'il faut reproduire ; Une checklist avant d'ouvrir une route d'upload personnalisée

1. **Appeler `wp_handle_upload()` plutôt que de traiter `$_FILES` à la main**, pour hériter automatiquement de l'ensemble des vérifications natives plutôt que de les réinventer partiellement :
`add_action( 'rest_api_init', function() {
    register_rest_route( 'redaction/v1', '/upload-image', array(
        'methods'             => 'POST',
        'callback'            => 'moncpt_upload_image_redaction',
        'permission_callback' => function() {
            return current_user_can( 'upload_files' );
        },
    ) );
} );

function moncpt_upload_image_redaction( WP_REST_Request $request ) {
    if ( empty( $_FILES['image'] ) ) {
        return new WP_Error( 'fichier_manquant', 'Aucun fichier reçu.', array( 'status' => 400 ) );
    }

    require_once ABSPATH . 'wp-admin/includes/file.php';
    require_once ABSPATH . 'wp-admin/includes/image.php';

    $resultat = wp_handle_upload( $_FILES['image'], array( 'test_form' => false ) );

    if ( isset( $resultat['error'] ) ) {
        return new WP_Error( 'upload_refuse', $resultat['error'], array( 'status' => 400 ) );
    }

    return new WP_REST_Response( array( 'url' => $resultat['url'] ), 201 );
}`
2. **Ne jamais omettre `permission_callback`**, en vérifiant une capacité réellement adaptée à l'upload (`upload_files`), jamais un simple `__return_true` laissé « pour tester » et oublié en production — une négligence malheureusement fréquente sur les routes développées dans l'urgence.
3. **Limiter explicitement la taille acceptée avant même d'appeler `wp_handle_upload()`**, indépendamment de la limite globale `upload_max_filesize` du serveur, pour éviter qu'un point d'entrée dédié à des photos de quelques mégaoctets n'accepte silencieusement un fichier de plusieurs centaines de mégaoctets et sature l'espace disque ou la mémoire du traitement d'image :
`if ( $_FILES['image']['size'] > 8 * MB_IN_BYTES ) {
    return new WP_Error( 'fichier_trop_volumineux', 'Image limitée à 8 Mo.', array( 'status' => 413 ) );
}`
4. **Restreindre la liste des types MIME acceptés à ce qui est réellement utile pour ce point d'entrée précis**, via le filtre `upload_mimes` appliqué localement à cette route plutôt que globalement à tout le site, pour éviter qu'un point d'entrée pensé pour des photos n'accepte aussi, par héritage de la configuration générale, des formats sans rapport avec l'usage (archives, documents exécutables sur certains systèmes) :
`add_filter( 'upload_mimes', function( $mimes ) {
    return array(
        'jpg|jpeg' => 'image/jpeg',
        'png'      => 'image/png',
        'webp'     => 'image/webp',
    );
}, 10, 1 );
// À restreindre au contexte de cette route précise via une vérification
// de l'action REST en cours, pour ne pas affecter le reste du site.`

## Un test simple pour vérifier qu'une route hérite bien de ces protections

Une fois la route corrigée, un test rapide avec `curl` confirme le comportement attendu, en tentant volontairement d'envoyer un fichier PHP renommé en `.jpg`, exactement le type de test qui aurait révélé la faille avant sa mise en production :

```
curl -X POST "https://exemple.fr/wp-json/redaction/v1/upload-image" \
  -H "Authorization: Bearer TOKEN_DE_TEST" \
  -F "image=@webshell-deguise.php.jpg;type=image/jpeg"
# Doit renvoyer une erreur explicite, jamais un succès avec une URL de fichier
```

> Sur les projets headless que nous auditons, la première question posée à propos de toute route d'upload personnalisée est invariablement la même : « appelle-t-elle `wp_handle_upload()`, ou traite-t-elle `$_FILES` à la main ? ». La seconde réponse déclenche systématiquement une revue approfondie.

## Ce que cette checklist ne couvre pas

Ce sujet ne traite pas de la sécurisation de l'authentification des routes REST elle-même (jetons d'application, JWT, OAuth), un préalable indispensable mais distinct, traité par ailleurs. Une route d'upload parfaitement protégée sur le plan des fichiers acceptés reste inutile si l'authentification qui la protège en amont est elle-même mal conçue.
