# Un formulaire de candidature HubSpot exposait des CV via une URL

> /cv/1042.pdf, puis /cv/1043.pdf : un identifiant séquentiel suffisait à parcourir les CV de tous les candidats d'un site carrière.

- Auteur : WordPress Développement
- Publié le : 2024-12-19
- Mis à jour le : 2024-12-19
- Catégorie : Sécurité
- URL : https://www.wpmoderne.fr/securite/formulaire-candidature-hubspot-cv-url-exposee/

## L’essentiel

- Un identifiant séquentiel dans une URL de fichier se devine trivialement
- Un jeton signé à durée limitée remplace l'identifiant prévisible sans changer le stockage
- Vérifier systématiquement les URLs générées par une intégration tierce avant mise en ligne

`/cv/1042.pdf`. Puis, en modifiant simplement l'identifiant dans la barre d'adresse, `/cv/1043.pdf`. Ce test, effectué par un candidat curieux après avoir reçu la confirmation de sa propre candidature sur le site carrière d'une entreprise, a suffi à révéler qu'il pouvait consulter le CV de n'importe quel autre postulant simplement en devinant un nombre. Le candidat a signalé le problème par email plutôt que de continuer à l'explorer, ce qui a permis une correction rapide avant toute exploitation plus large.

Le site carrière reposait sur un formulaire de candidature HubSpot, dont les pièces jointes téléversées par les candidats étaient rapatriées et stockées localement sur le site WordPress pour être consultées par l'équipe de recrutement depuis un tableau de bord interne. C'est cette étape de rapatriement local qui avait introduit la faille : chaque fichier était enregistré avec un identifiant auto-incrémenté, exposé tel quel dans l'URL de téléchargement.

## Une référence directe non sécurisée, classique et pourtant fréquente

Le code qui gérait la réception des candidatures HubSpot, via un webhook déclenché à chaque nouvelle soumission, enregistrait le CV téléversé de cette manière :

```
function enregistrer_cv_candidat( $url_source_hubspot, $candidat_id ) {
    global $wpdb;

    $chemin_destination = WP_CONTENT_DIR . '/cv/' . $candidat_id . '.pdf';
    file_put_contents( $chemin_destination, wp_remote_retrieve_body(
        wp_remote_get( $url_source_hubspot )
    ) );

    return home_url( '/cv/' . $candidat_id . '.pdf' );
}
```

`$candidat_id` correspondait à l'identifiant auto-incrémenté de la table de candidatures en base de données, directement réutilisé comme nom de fichier et comme URL publique. Aucune vérification d'autorisation n'intervenait au moment du téléchargement : le fichier étant physiquement accessible dans le répertoire `uploads`, quiconque connaissait ou devinait l'identifiant pouvait le récupérer directement, sans passer par aucune authentification.

## Le jeton signé à durée limitée, correctif retenu

> L'essentiel à retenir : Un identifiant séquentiel dans une URL de fichier se devine trivialement ; Un jeton signé à durée limitée remplace l'identifiant prévisible sans changer le stockage ; Vérifier systématiquement les URLs générées par une intégration tierce avant mise en ligne

La correction ne change pas le stockage physique des fichiers, mais remplace l'URL prévisible par un jeton signé, généré à la demande et valable uniquement pour une courte durée, vérifié avant toute diffusion du contenu du fichier :

```
function generer_lien_cv( $candidat_id, $duree_validite = 3600 ) {
    $expiration = time() + $duree_validite;
    $signature  = hash_hmac( 'sha256', $candidat_id . '|' . $expiration, CLE_SIGNATURE_CV );

    return add_query_arg( array(
        'candidat'    => $candidat_id,
        'expire'      => $expiration,
        'signature'   => $signature,
    ), home_url( '/telecharger-cv/' ) );
}

function verifier_et_servir_cv( WP_REST_Request $request ) {
    $candidat_id = absint( $request->get_param( 'candidat' ) );
    $expiration  = absint( $request->get_param( 'expire' ) );
    $signature   = $request->get_param( 'signature' );

    $signature_attendue = hash_hmac( 'sha256', $candidat_id . '|' . $expiration, CLE_SIGNATURE_CV );

    if ( ! hash_equals( $signature_attendue, $signature ) || time() > $expiration ) {
        return new WP_Error( 'lien_invalide', 'Ce lien a expiré ou est invalide.', array( 'status' => 403 ) );
    }

    if ( ! current_user_can( 'edit_users' ) ) {
        return new WP_Error( 'acces_refuse', 'Accès réservé au recrutement.', array( 'status' => 403 ) );
    }

    // lecture et diffusion sécurisée du fichier stocké hors du répertoire public
}
```

Deux vérifications se combinent désormais : la signature garantit que le lien a bien été généré par le site lui-même pour ce candidat précis et n'a pas expiré, et le contrôle de capacité garantit que seul un compte de l'équipe de recrutement peut effectivement accéder au fichier, même en possession d'un lien valide. Le fichier physique a par ailleurs été déplacé hors du répertoire `uploads` public, pour empêcher tout accès direct qui contournerait entièrement ce mécanisme.

## Vérifier les URLs générées par toute intégration tierce

Ce cas illustre un point de vigilance qui dépasse largement HubSpot : chaque fois qu'une intégration tierce rapatrie ou génère des fichiers stockés localement sur un site WordPress, l'URL résultante mérite un examen spécifique, indépendamment de la confiance accordée à l'outil tiers lui-même. La checklist suivante s'applique à toute intégration de ce type :

- L'URL du fichier généré contient-elle un identifiant prévisible ou séquentiel ?
- Le fichier est-il accessible sans aucune vérification d'authentification une fois l'URL connue ?
- Le fichier est-il stocké dans un répertoire directement accessible publiquement par le serveur web ?

Une réponse positive à l'une de ces trois questions signale un risque de référence directe non sécurisée, une catégorie de vulnérabilité qui touche rarement le cœur de WordPress mais très souvent les intégrations construites autour de lui.

> Un identifiant auto-incrémenté est excellent pour une clé primaire de base de données ; il ne devrait jamais servir directement de nom de fichier accessible publiquement.

## Ce qu'on retient

La faille n'a pas été introduite par HubSpot, qui gère ses propres pièces jointes de façon sécurisée sur ses serveurs, mais par l'étape de rapatriement local ajoutée pour donner à l'équipe de recrutement un accès simplifié depuis le site WordPress. Toute étape ajoutée entre un service tiers fiable et son usage final mérite le même niveau de vigilance que si elle avait été construite de zéro, sans hériter par défaut de la confiance accordée au service d'origine.
