Sécuriser les envois de fichiers : contrôles navigateur, validation serveur et tests cURL

Un champ d’envoi de fichier paraît simple : choisir une photo, cliquer sur Envoyer et attendre la confirmation. Accepter des octets fournis par un utilisateur implique pourtant plusieurs décisions sur le format, l’identité, la taille, le stockage, le traitement et les téléchargements ultérieurs.

Un bon parcours doit être agréable à utiliser tout en résistant aux erreurs et aux entrées malveillantes. Les contrôles du navigateur améliorent les messages destinés à l’utilisateur. Les contrôles du serveur établissent la frontière de sécurité. Confondre les deux est une erreur fréquente.

Ce guide explique comment organiser cette frontière, construire des requêtes multipart correctes et tester un point d’accès avec cURL, sans supposer qu’une extension décrit fidèlement le contenu.

Définir une politique d’envoi précise

Avant d’écrire le formulaire, décidez ce que l’application accepte. Pour une photo de profil, une politique pourrait être :

DécisionExemple de règle
FinalitéUne photo de profil par compte autorisé
Formats acceptésJPEG et PNG uniquement
Taille maximale5 Mio par fichier
Nombre de fichiersUn par requête
Nom dans le stockageIdentifiant aléatoire généré par l’application
StockageStockage objet privé, hors d’un répertoire Web permettant l’exécution
TraitementDécodage, validation, réencodage et suppression des métadonnées inutiles
AccèsPropriétaire et personnel autorisé uniquement

Ces valeurs sont des exemples. Un éditeur vidéo, un portail médical et un outil d’avatars ont des besoins différents. Définissez pourquoi chaque format est nécessaire et retirez les autres de la liste autorisée.

La fiche OWASP sur l’envoi de fichiers (en anglais) recommande plusieurs couches de protection : extensions autorisées, vérification du contenu, noms générés, quotas, autorisations et stockage séparé.

Étape 1 : informer rapidement dans le navigateur

Commencez par un champ accessible et une zone de message :

<form action="/api/profile/photo" method="post" enctype="multipart/form-data">
  <label for="photo">Profile photo (JPEG or PNG, up to 5 MiB)</label>
  <input id="photo" name="photo" type="file"
         accept="image/jpeg,image/png" required>
  <p id="photo-status" role="status" aria-live="polite"></p>
  <button type="submit">Upload photo</button>
</form>

L’attribut accept aide le sélecteur de fichiers à proposer les formats souhaités. Ce n’est pas un filtre de sécurité. Le navigateur ou le système peut permettre de le contourner, et une requête HTTP peut être envoyée sans utiliser votre formulaire.

Ajoutez un contrôle pour éviter de transmettre un fichier manifestement trop volumineux :

const photo = document.querySelector("#photo");
const status = document.querySelector("#photo-status");
const maxBytes = 5 * 1024 * 1024;
const permittedTypes = new Set(["image/jpeg", "image/png"]);

photo.addEventListener("change", () => {
  const file = photo.files?.[0];
  status.textContent = "";
  photo.setCustomValidity("");
  if (!file) return;

  if (file.size > maxBytes) {
    photo.setCustomValidity("The file exceeds 5 MiB.");
  } else if (!permittedTypes.has(file.type)) {
    photo.setCustomValidity("Choose a JPEG or PNG image.");
  }

  if (!photo.checkValidity()) {
    status.textContent = photo.validationMessage;
  } else {
    status.textContent = `Ready to upload: ${file.name}`;
  }
});

Ces vérifications servent uniquement à améliorer l’utilisation. La valeur file.type et le nom affiché ne prouvent pas la nature des octets. Effectuez toujours la validation réelle sur le serveur.

Vérifiez aussi le texte et le contraste des messages d’erreur. Color Studio aide à choisir des couleurs accessibles. Le guide sur l’échappement et l’assainissement HTML explique pourquoi les noms de fichiers non fiables doivent être affichés comme du texte, sans innerHTML.

Étape 2 : envoyer le fichier en multipart

Un formulaire HTML avec enctype="multipart/form-data" envoie une ou plusieurs parties nommées. Chacune contient des métadonnées et des octets, séparés par une délimitation générée par le client.

Pour gérer l’envoi en JavaScript, utilisez FormData :

const form = document.querySelector("form");
form.addEventListener("submit", async (event) => {
  event.preventDefault();
  if (!form.reportValidity()) return;

  const body = new FormData(form);
  const response = await fetch(form.action, {
    method: "POST",
    body,
    credentials: "same-origin"
  });

  const status = document.querySelector("#photo-status");
  status.textContent = response.ok
    ? "Photo uploaded successfully."
    : "Upload failed. Review your file and try again.";
});

Ne définissez pas manuellement Content-Type: multipart/form-data avec FormData et fetch(). Le navigateur doit ajouter le paramètre boundary approprié. Un en-tête sans la délimitation correspondante peut empêcher le serveur d’analyser la requête.

Cet exemple est volontairement court. En production, interceptez aussi les erreurs réseau, empêchez les envois en double lorsque cela est nécessaire et affichez des messages précis sans divulguer d’informations internes. Ajoutez les protections de session et contre les attaques CSRF habituelles de l’application : credentials: "same-origin" ne remplace pas une protection CSRF.

Le guide JSON, formulaires et multipart avec cURL compare ces formats de corps de requête.

Étape 3 : valider sur le serveur

Traitez chaque envoi comme une entrée non fiable. Voici une séquence de contrôles :

  1. Authentifier l’utilisateur et vérifier son autorisation pour cette action.
  2. Limiter le corps de la requête avant ou pendant sa lecture, pour rejeter rapidement les envois trop volumineux.
  3. Limiter le nombre de parties et de fichiers pour éviter l’épuisement des ressources.
  4. Normaliser et vérifier les extensions autorisées, sans s’arrêter là.
  5. Examiner la signature réelle du fichier et l’analyser avec une bibliothèque maintenue ; les en-têtes MIME peuvent être falsifiés.
  6. Appliquer la validation et le traitement propres au format. Pour une image, décoder et réencoder en limitant les dimensions en pixels et les ressources utilisées.
  7. Générer un nom aléatoire sur le serveur plutôt que de faire confiance à ../../avatar.php ou à un nom Unicode ambigu.
  8. Stocker hors d’un répertoire permettant l’exécution, ou dans un stockage objet isolé avec des droits explicites.
  9. Renvoyer un identifiant opaque plutôt qu’un chemin de fichier prévisible.
  10. Enregistrer un événement d’audit limité et nettoyer les ressources après un échec, sans divulguer de chemins internes.

Un JPEG valide peut occuper énormément de mémoire une fois décodé. Une archive peut aussi se décompresser en un volume considérable. Les limites doivent couvrir le traitement, pas seulement les octets compressés reçus. Selon le risque métier, envisagez analyse antivirus, neutralisation du contenu, quarantaine et contrôle manuel.

Une signature reconnue ne garantit pas qu’un fichier soit sûr. Elle n’est qu’un contrôle parmi d’autres, et les analyseurs peuvent eux-mêmes contenir des vulnérabilités.

Le récent article sur les correctifs de sécurité PHP rappelle qu’une logique applicative correcte ne dispense pas de mettre à jour l’environnement d’exécution, les outils de traitement d’images et les bibliothèques d’envoi.

Étape 4 : tester le point d’accès avec cURL

Le navigateur peut rejeter un fichier avant même qu’il atteigne le serveur. Pour vérifier les contrôles serveur, envoyez directement une requête depuis votre poste ou un environnement de test autorisé :

curl --include --fail-with-body \
  --request POST \
  --url 'https://staging.example.com/api/profile/photo' \
  --form 'photo=@./sample.png;type=image/png'

Utilisez un fichier de test jetable, sans données sensibles. --form construit une requête multipart et sa délimitation. Ajoutez l’authentification légitime de votre préproduction sans coller de secrets dans des journaux partagés ou des captures.

Essayez ensuite des cas négatifs contrôlés : fichier trop volumineux, texte renommé en PNG, requête non autorisée, fichier supplémentaire inattendu et contenu qui ne correspond pas au type MIME annoncé. Relevez le statut et le corps de réponse. Préférez des erreurs stables et claires, par exemple 413 pour une requête trop volumineuse ; le choix précis des statuts dépend de l’application.

Le générateur de commandes cURL prépare méthodes, en-têtes et corps de requête sans effectuer l’envoi. Le guide cURL explique les options et les règles de citation du shell. Pour les pièces jointes multipart, ajoutez --form dans le terminal comme ci-dessus.

N’utilisez pas --insecure uniquement pour masquer une erreur TLS pendant un test de sécurité. Corrigez le certificat de test ou configurez la confiance appropriée.

Étape 5 : comprendre CORS sans affaiblir le serveur

Si l’interface et l’API sont sur des origines différentes, le navigateur peut envoyer une requête préliminaire CORS selon la méthode, les en-têtes et le type de corps. Les cookies seuls ne déclenchent pas cette requête ; un en-tête Authorization, oui. Une requête avec identifiants exige aussi une origine explicitement autorisée et l’en-tête de réponse approprié pour ces identifiants.

CORS ne détermine pas si le fichier est sûr ni si l’utilisateur est autorisé. Il contrôle l’accès d’un script à une réponse provenant d’une autre origine. Un client hors navigateur peut toujours appeler le serveur. Continuez à vérifier l’identité, les protections CSRF pertinentes, les méthodes, les quotas et le contenu.

Ajouter Access-Control-Allow-Origin: * partout est une erreur fréquente. Cette valeur est incompatible avec les requêtes navigateur avec identifiants et peut détourner l’attention d’un défaut d’autorisation. Consultez le guide de dépannage des requêtes préliminaires CORS avant de changer les règles d’accès.

Comparez la requête préliminaire, l’envoi et la réponse dans le panneau Réseau. Identifiez la requête qui échoue avant d’attribuer toute erreur aux octets du fichier.

Étape 6 : protéger aussi les téléchargements

L’envoi ne représente que la moitié du cycle. Un point d’accès de téléchargement doit vérifier l’autorisation indépendamment, même si le navigateur connaît déjà l’identifiant de l’objet. Évitez les URL publiques prévisibles pour des documents privés. Définissez les en-têtes Content-Type et Content-Disposition adaptés et envisagez X-Content-Type-Options: nosniff selon le contexte.

Ne servez pas du HTML ou du SVG arbitraire fourni par un utilisateur directement sur l’origine de confiance de l’application. Du contenu actif pourrait s’y exécuter. Une origine de téléchargement isolée ou une remise en pièce jointe contribue à réduire ce risque. Si un document doit avoir une URL publique, décidez explicitement de sa visibilité, de sa révocation, de son indexation et de sa mise en cache.

Protégez séparément l’environnement de préproduction lui-même. Le guide pour protéger un site Apache avant une présentation client fournit un point de départ. L’authentification HTTP Basic autour de cet environnement est une protection supplémentaire, pas un remplacement de l’autorisation d’envoi.

Une liste de tests réaliste

Vérifiez les succès et les rejets avant la mise en service :

TestRésultat attendu
Petit PNG valide envoyé par un utilisateur autoriséStockage avec un identifiant généré
Fichier au-dessus de la limiteRejet avec une erreur claire
Nom .png avec des octets sans rapportRejet après inspection du contenu
Content-Type incorrect ou falsifiéAucun contournement des contrôles serveur
Envoi sans authentification ou autorisationRejet avant stockage
Parties ou fichiers supplémentairesRejet ou traitement selon une règle explicite
Document privé demandé par un autre utilisateurAccès refusé
Envoi interrompu ou traitement en échecNettoyage des ressources partielles

Un envoi sécurisé ne repose pas sur une seule expression régulière. Le formulaire, le gestionnaire HTTP, l’analyseur, la bibliothèque de traitement, le stockage et le téléchargement doivent appliquer des règles cohérentes.

Pour aller plus loin

Essayez sur une API de préproduction autorisée : préparez la requête avec le générateur de commandes cURL, ajoutez un fichier de test avec --form et vérifiez que le serveur rejette les contenus incorrects même sans les contrôles du navigateur.

L'offre Premium arrive bientôt

Un abonnement Premium facultatif permettant de supprimer les publicités est prévu. Les abonnements ne sont pas encore disponibles.

Vous pouvez continuer à utiliser nos outils gratuits sans compte.