Accueil / Articles

De l’upload à l’URL : stocker et servir en toute sécurité les fichiers des utilisateurs dans Express

Apprenez où les applications Express doivent stocker les fichiers téléchargés, comment express.static associe un dossier à des URL, et quels mécanismes de protection empêchent que les téléchargements des utilisateurs ne deviennent une faille de sécurité.

Éditeur : REACTAPP.TOP7 min de lecture1589 mots

Sources: Storing Uploaded Files and Serving Them in Express

Un utilisateur choisit une photo de profil, clique sur « télécharger », et une seconde plus tard elle apparaît sur sa page. Derrière cette petite interaction se cachent trois décisions de conception réelles : l’endroit où le fichier est physiquement stocké, la manière dont le serveur le retrouve, et la façon dont le navigateur finit par l’afficher. Ce guide aborde ces trois aspects pour une application Express, depuis le choix d’un emplacement de stockage jusqu’à la mise à disposition des fichiers via une URL, et se termine par les précautions nécessaires pour éviter que les téléchargements ne deviennent une surface d’attaque.

Stocker les téléchargements sur le disque

Au préalable, un fichier doit être enregistré quelque part avant de pouvoir être à nouveau servi. Dans la configuration la plus simple, il s’agit d’un dossier dans le système de fichiers du serveur, généralement nommé uploads et placé à la racine du projet, à côté du code source, et non à l’intérieur :

project-root/
  uploads/
    avatars/
      user-42.png
      user-91.png
    documents/
      invoice-108.pdf
  src/
    app.js

Regrouper les fichiers par fonction, tels que avatars/ et documents/, apporte rapidement des avantages. Cela permet de garder la liste des dossiers gérable à mesure que leur nombre augmente, facilite l’application de politiques de nettoyage et de sauvegarde par catégorie, et, comme vous le verrez bientôt, génère des URL faciles à prédire. Un seul dossier plat contenant tous les types de fichiers devient difficile à gérer bien avant de poser un problème de performance.

Dans la plupart des applications, la base de données ne stocke qu’une référence au fichier, comme le chemin relatif avatars/user-42.png, et non le fichier lui-même ni une URL complète. En stockant le chemin relatif, il est possible de changer plus tard le domaine ou de passer à un autre système de stockage sans avoir à réécrire chaque ligne.

Disque local ou stockage d’objets externe

Garder les fichiers sur le serveur d’application

Écrire des fichiers directement sur le disque du serveur, comme indiqué dans la configuration ci-dessus, ne nécessite aucune infrastructure supplémentaire. Cela convient bien au développement local, aux prototypes, aux outils internes et aux applications dont le volume de téléchargement est faible et prévisible.

Déplacer les fichiers vers un service de stockage

Les systèmes en production confient le plus souvent les téléchargements à un stockage d’objets dédié, comme Amazon S3, Google Cloud Storage ou un service similaire, afin de ne pas impliquer le disque du serveur d’application.

Pourquoi ce choix est important

  • Les disques peuvent être de petite taille et avoir une durée de vie limitée. De nombreuses plateformes d’hébergement réinstallent ou remplacent les instances à chaque déploiement, de sorte qu’un fichier qui n’existe que sur le disque local peut disparaître lors du prochain déploiement.
  • Les instances multiples ne partagent pas de disque. Une fois l’échelle horizontale activée, un fichier enregistré sur une instance reste invisible pour les autres. Un stockage centralisé offre à chaque instance la même vue.
  • Les systèmes de stockage d’objets ont été conçus pour cela. La durabilité, la réplication, les sauvegardes et une livraison efficace à grande échelle en constituent l’objectif principal, tandis qu’un serveur d’applications polyvalent est optimisé pour exécuter votre code.
  • Rien de tout cela ne rend le stockage local incorrect. Pour les projets d’apprentissage et les applications vraiment petites, c’est une première étape judicieuse, et les principes présentés ci-dessous (chemins organisés, URLs prévisibles, validation rigoureuse) restent inchangés si vous migrez ultérieurement vers un stockage externe. En plaçant le code de gestion du stockage derrière un petit module contenant des fonctions comme save et getUrl, cette migration devient beaucoup moins difficile.

    Utiliser express.static pour servir le dossier

    Lorsque le fichier est enregistré, le navigateur a toujours besoin d’un moyen de le récupérer. Express inclut un middleware pour les fichiers statiques précisément à cette fin :

    const express = require("express");
    const app = express();
    
    app.use("/uploads", express.static("uploads"));
    

    Cette ligne monte le répertoire uploads sous le préfixe URL /uploads. Pour toute requête dont l’URL commence par ce préfixe, Express cherche un fichier à l’emplacement correspondant dans le dossier et, s’il existe, le transmet en flux avec un type de contenu approprié ainsi que des en-têtes de mise en cache. Vous n’avez pas besoin d’écrire un gestionnaire pour chaque fichier.

    Cela représente un modèle de pensée différent de celui d’une route API ordinaire. Au lieu d’exécuter une logique qui charge des données et génère une réponse, vous publiez tout un répertoire, en fonction uniquement de son chemin. Deux remarques pratiques en découlent :

    • L’argument du dossier est résolu par rapport au répertoire depuis lequel le processus Node a été lancé, et non par rapport au fichier contenant le code. L’utilisation d’un chemin absolu, comme celui généré avec path.join(__dirname, „..“, „uploads“), évite les surprises lorsque l’application est lancée depuis un répertoire de travail différent.
    • Tout ce qui est placé dans ce dossier devient public. Montez uniquement le répertoire qui doit être exposé.

    Comment un fichier enregistré devient une URL

    Lorsque le service de diffusion statique est configuré, le chemin sur le disque et le chemin dans l’URL se correspondent :

    File on disk:     uploads/avatars/user-42.png
    Accessible at:    https://yourapp.com/uploads/avatars/user-42.png
    

    Grâce à cette correspondance un-à-un, le frontend peut faire référence directement au fichier, par exemple dans une balise d’image, sans avoir besoin d’un point de terminaison API dédié pour télécharger les données :

    <img src="https://yourapp.com/uploads/avatars/user-42.png" alt="User avatar" />
    

    C’est ici que la structure des dossiers décrite dans la première section devient utile. Le format choisi sur le disque correspond exactement à celui que les clients voient dans les URL ; donc une structure ordonnée et cohérente permet d’obtenir des liens similaires. Cela signifie également que l’URL peut être générée à partir du chemin relatif stocké dans la base de données, ce qui permet de regrouper le domaine et le préfixe dans une seule valeur de configuration.

    Précautions de sécurité pour les téléchargements des utilisateurs

    Les téléchargements sont une cible privilégiée pour les abus, donc le simple fait que « le téléchargement fonctionne » n’est qu’un point de départ. Chacune des mesures suivantes comble une faille spécifique.

    Vérifier ce que le fichier est réellement

    Une extension de nom de fichier n’est rien de plus que du texte choisi par l’utilisateur et ne prouve rien. Vérifiez le véritable type de contenu, de préférence en lisant les premiers octets du fichier plutôt que de vous fier au type MIME déclaré, et n’acceptez que ce dont la fonction a besoin. Un point de terminaison pour des avatars, par exemple, ne devrait accepter que des images et rien d’autre. Des formats tels que SVG et HTML exigent une prudence particulière, car les navigateurs peuvent exécuter des scripts intégrés en eux.

    Limitez la taille

    Fixez une taille maximale pour chaque téléchargement. Sans limite, une seule demande de trop grande taille, qu’elle soit accidentelle ou intentionnelle, peut remplir le disque ou saturer la bande passante. Appliquez cette limite lors de l’analyse de la demande, afin que le serveur cesse de lire précocement au lieu d’accepter d’abord l’intégralité du contenu.

    Générez vos propres noms de fichiers

    N’écrivez jamais un fichier sous le nom fourni par le client. Ce nom peut contenir des caractères étranges ou des séquences de navigation dans le chemin comme ../../config.js, destinées à contourner le répertoire de téléchargement. Créez un nom nouveau sur le serveur, par exemple un identifiant aléatoire accompagné d’une extension vérifiée. Par conséquent, cela empêche également deux utilisateurs qui téléchargent tous deux photo.jpg de se recouvrir mutuellement.

    Isolez le répertoire de téléchargement

    Stockez les fichiers téléchargés dans un répertoire clairement séparé du code source et des configurations, et assurez-vous que le middleware statique ne expose que ce répertoire. Un montage accidentel de la racine du projet exposerait les fichiers d’environnement et le code source à quiconque devine le bon chemin.

    Réfléchissez à l’origine qui sert les fichiers

    Fournir du contenu fourni par l’utilisateur provenant du même domaine que l’application principale peut entraîner des problèmes tels que le scripting entre sites, car un fichier malveillant s’exécuterait avec l’origine de votre site. Pour cette raison, de nombreux systèmes en production transmettent les fichiers téléchargés depuis un stockage externe ou un sous-domaine distinct. L’envoi de l’en-tête X-Content-Type-Options: nosniff et, pour les téléchargements qui ne doivent pas s’afficher en ligne, de l’en-tête Content-Disposition: attachment, réduit encore davantage le risque.

    Questions fréquentes

    Tout projet doit-il utiliser S3 ou un service similaire ?

    Pas nécessairement. Pour de petits projets ou des projets d’apprentissage, le disque local est plus simple et tout à fait suffisant. Le stockage externe devient important lorsque vous déployez sur une plateforme disposant d’un système de fichiers éphémère ou lorsque vous exécutez plusieurs instances qui doivent accéder aux mêmes fichiers.

    express.static a-t-il besoin d’un paquet supplémentaire ?

    Non, c’est intégré à Express. Le parsing des fichiers téléchargés provenant d’un formulaire multipart nécessite généralement une bibliothèque comme Multer, mais le service des fichiers déjà enregistrés ne l’exige pas.

    Est-il acceptable de conserver le nom d’origine du fichier ?

    Il vaut mieux ne pas le faire. Un nom généré par le serveur évite les caractères étranges, les conflits de noms identiques ainsi que les manipulations cachées dans un nom de fichier malveillant. Si les utilisateurs doivent voir le nom d’origine, stockez-le plutôt en tant que métadonnées dans la base de données.

    Le accès aux fichiers téléchargés peut-il être restreint ?

    Oui, mais pas uniquement avec express.static, qui remet un fichier à quiconque connaît son URL. Les fichiers privés exigent une route dédiée qui vérifie l’authentification et l’autorisation avant de diffuser le fichier, ou, en cas d’utilisation d’un stockage externe, des liens à durée de vie limitée générés uniquement pour les utilisateurs autorisés.

    En résumé

    express.static associé à un préfixe URL clair, ou via une route protégée pour les fichiers privés ? Et qu’est-ce qui le rend sûr : des types vérifiés, des limites de taille, des noms générés par le serveur et un emplacement de service à portée restreinte, de préférence séparé. Une fois ces éléments définis, le reste de la fonctionnalité relève principalement d’aspects techniques.

    Lectures complémentaires