Directives

Téléchargement de fichier

Il est très courant que les applications aient, à un moment ou à un autre, besoin de permettre aux utilisateurs de télécharger un fichier (pour l'utiliser ou simplement le stocker) quelque part dans l'application. Bien que cela semble simple, la manière dont cette fonction est implémentée peut s'avérer critique en raison des risques potentiels associés au traitement des téléchargements de fichiers.

Jetez un œil à cet exemple rapide pour mieux visualiser ce que nous voulons dire.

Imaginons une application qui permet aux utilisateurs de télécharger une photo de profil :

public string UploadProfilePicture(FormFile uploadedFile)
{
    // Générer le chemin où enregistrer le fichier téléchargé
    var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";  

    // Enregistrer le fichier
    var localFile = File.OpenWrite(path);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Mettre à jour la photo de profil
    UserProfile.UpdateUserProfilePicture(request.User, path)

    return path;
}

Il s'agit d'une fonction de téléchargement très basique qui est également vulnérable au parcours de répertoire (Path Traversal).

Selon l'implémentation exacte de l'application, un attaquant pourrait télécharger une autre page ou un script (pensez aux fichiers .asp, .aspx ou .php) permettant d'appeler et d'exécuter du code arbitraire. Cela pourrait également permettre d'écraser des fichiers existants.

Problème 1 - Enregistrement sur le disque local plutôt que sur un stockage de données externe

Avec la généralisation des services cloud, la livraison des applications dans des conteneurs et la standardisation des configurations à haute disponibilité, la pratique consistant à écrire les fichiers téléchargés sur le disque local de l'application devrait être évitée à tout prix.

Dans la mesure du possible, les fichiers doivent être téléchargés vers un stockage centralisé (stockage par blocs ou base de données). Cela permet d'éviter des catégories entières de vulnérabilités de sécurité.

Problème 2 - Absence de validation des extensions

Dans de nombreux cas d'exploitation de vulnérabilité liée au téléchargement de fichiers, l'attaque repose sur la possibilité de télécharger un fichier avec une extension spécifique. Il est donc fortement conseillé d'utiliser une « liste blanche » d'extensions autorisées pour les fichiers pouvant être téléchargés.

Assurez-vous d'utiliser les méthodes fournies par votre langage ou framework pour obtenir l'extension du fichier afin d'éviter des problèmes tels que l'injection de caractères nuls (null-byte injection).

Il peut être tentant de valider le type de contenu (content type) du téléchargement, mais cela peut rendre le système très fragile, car les types de contenu utilisés pour des fichiers spécifiques peuvent varier d'un système d'exploitation à l'autre. De plus, cela ne vous renseigne en rien sur le fichier lui-même, puisque le type de contenu n'est qu'une simple correspondance basée sur l'extension.

Problème 3 - Absence de prévention contre le parcours de répertoire

Un autre problème courant avec les téléchargements de fichiers est leur vulnérabilité au parcours de répertoire. C'est un sujet vaste en soi, donc plutôt que d'essayer de le résumer ici, consultez notre guide complet sur le Parcours de répertoire.

Plus d'exemples

Vous trouverez ci-dessous quelques exemples supplémentaires de téléchargements de fichiers, sécurisés et non sécurisés, que vous pouvez examiner.

C# - Non sécurisé

public string UploadProfilePicture(IFormFile uploadedFile)
{
    // Générer le chemin d'enregistrement du fichier
    var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";

    // Enregistrer le fichier
    var localFile = File.OpenWrite(path);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Mettre à jour la photo de profil
    UserProfile.UpdateUserProfilePicture(request.User, path)

    return path;
}

C# - Sécurisé

public List<string> AllowedExtensions = new() { ".png", ".jpg", ".gif"};

public string UploadProfilePicture(IFormFile uploadedFile)
{
    // REMARQUE : L'idéal est d'éviter d'enregistrer des fichiers sur le disque local.
    var basePath = Path.GetFullPath("./uploads/avatars/");

    // Empêcher le parcours de répertoire en ignorant le nom de fichier fourni. Évite aussi les conflits.
    var newFileName = GenerateFileName(uploadedFile.FileName);

    // Générer le chemin d'enregistrement du fichier
    var canonicalPath = Path.Combine(basePath, newFileName);

    // S'assurer de ne pas enregistrer accidentellement en dehors du dossier de base
    if(!canonicalPath.StartsWith(basePath))
    {
        return BadRequest("Tentative d'enregistrement hors du dossier autorisé");
    }

    // Vérifier que l'extension est autorisée
    if(!IsFileAllowedExtension(uploadedAllowedExtensions))
    {
        return BadRequest("Extension non autorisée");
    }

    // Enregistrer le fichier
    var localFile = File.OpenWrite(canonicalPath);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Mettre à jour la photo de profil
    UserProfile.UpdateUserProfilePicture(request.User, canonicalPath)

    return path;

public bool GenerateFileName(string originalFileName) {
    return $"{Guid.NewGuid()}{Path.GetExtension(originalFileName)}";
}

public bool IsFileAllowedExtension(string fileName, List<string> extensions) {
    return extensions.Contains(Path.GetExtension(fileName));
}

‍

Java - Non sécurisé

@Controller
public class FileUploadController {

   @RequestMapping(value = "/files/upload", method = RequestMethod.POST)
   @ResponseBody
   public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file, @AuthenticationPrincipal User user) {

       try {

           String uploadPath = "./uploads/avatars/" + principal.getName() + "/" + file.getOriginalFilename();

           File transferFile = new File(uploadPath);
           file.transferTo(transferFile);

       } catch (Exception e) {
           return new ResponseEntity<>("Erreur d'upload", HttpStatus.INTERNAL_SERVER_ERROR);
       }

       return new ResponseEntity<>(uploadPath, HttpStatus.CREATED);
   }
}

Java - Sécurisé

@Controller
public class FileUploadController {

    @RequestMapping(value = "/files/upload", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file, @AuthenticationPrincipal User user) {

        try {
            String baseFolder = Paths.get("./uploads/avatars/").normalize();
            String uploadPath = Paths.get(baseFolder.toString() +
GenerateFileName(file.getOriginalFilename())).normalize();
           // Vérifier que l'extension est autorisée
            if(!IsAllowedExtension(file.getOriginalFilename()) {
                return new ResponseEntity<>("Extension non autorisée", HttpStatus.FORBIDDEN);
            }

            // Vérifier que le fichier n'est pas enregistré hors du dossier racine
           if(!uploadPath.toString().startsWith(baseFolder.toString()))            {
                return new ResponseEntity<>("Enregistrement hors du dossier racine interdit.", HttpStatus.FORBIDDEN);
           }

            File transferFile = new File(uploadPath.toString());
            file.transferTo(uploadPath.toString());

        } catch (Exception e) {
            return new ResponseEntity<>("Erreur d'upload", HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return new ResponseEntity<>(uploadPath, HttpStatus.CREATED);
    }

    private string GenerateFileName(String fileName) {
        return UUID.randomUUID().toString() + "." + FilenameUtils.getExtension(fileName);
    }

    private boolean IsAllowedExtension(String fileName) {
        String[] allowedExtensions = {"jpg", "png", "gif"};
        String extension = FilenameUtils.getExtension(filename);
        return allowedExtensions.contains(extension);
    }
}

Python - Flask - Non sécurisé

@app.route('/files/upload', methods=['POST'])
def upload_file():

    file = request.files['file']

    savedFilePath = os.path.join("./uploads/avatars/", file.filename)
    file.save(savedFilePath)

    return savedFilePath

Python - Flask - Sécurisé

@app.route('/files/upload', methods=['POST'])
def upload_file():

    file = request.files['file']
    baseFolder = os.path.normpath("./uploads/avatars/")
    savedFilePath = os.path.normpath(os.path.join(baseFolder, generate_file_name(file.filename)))

    # Vérifier que l'extension est autorisée
    if not is_extension_allowed(file.filename):
        return "Extension non autorisée"

    # Vérifier que le fichier n'est pas enregistré hors du dossier de base
    if not savedFilePath.startsWith(baseFolder):
        return "Tentative d'enregistrement hors du dossier de base"

    file.save(savedFilePath)

    return savedFilePath

def generate_file_name(filename):
    return str(uuid.uuid4()) + os.path.splitext(filename)[1]

def is_extension_allowed(filename):
    return os.path.splitext(filename)[1] in (".png", ".jpg", ".gif")
Slogan

Le codage sécurisé en pratique

Explorez nos directives de développement sécurisé pour comprendre et atténuer les vulnérabilités logicielles telles que l'OWASP Top 10, et plongez dans des missions de formation guidées pour vous exercer sur des simulations d'applications réelles.

Autres directives
Réserver une démo