Lignes directrices

Téléchargement de fichiers

Il est très fréquent que les applications doivent, à un moment ou à un autre, permettre aux utilisateurs de télécharger un fichier (soit pour l'utiliser, soit pour le stocker) quelque part dans l'application. Bien que cela semble assez simple, la manière dont cette fonction est mise en œuvre peut être très critique en raison des risques potentiels associés à la manière dont les téléchargements de fichiers sont gérés. 

Jetez un coup d'œil à cet exemple rapide, pour mieux comprendre ce que nous voulons dire. 

Supposons qu'il s'agisse d'une application permettant aux utilisateurs de télécharger une photo de profil :

public string UploadProfilePicture(FormFile uploadedFile)
{
    // Generate path to save the uploaded file at
    var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";  

    // Save the file
    var localFile = File.OpenWrite(path);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Update the profile picture
    UserProfile.UpdateUserProfilePicture(request.User, path)

    return path;
}

Il s'agirait d'une fonction de téléchargement très basique qui s'avère également vulnérable au détournement de chemin. 

En fonction de l'implémentation exacte de l'application, un attaquant pourrait télécharger une autre page/script (pensez aux fichiers .asp, .aspx, ou .php) qui permettrait d'appeler directement et d'exécuter un code arbitraire. Cela pourrait également permettre de remplacer des fichiers existants. 

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

À mesure que l'utilisation des services en nuage se banalise, les applications sont livrées dans des conteneurs, les configurations à haute disponibilité sont devenues la norme et la pratique consistant à écrire les fichiers téléchargés sur le disque local de l'application devrait être évitée à tout prix. 

Les fichiers doivent être téléchargés vers une forme de stockage central si possible (stockage en bloc ou base de données). Cela permet d'éviter des catégories entières de failles de sécurité dans ce cas. 

Problème 2 - Non validation des extensions 

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

Veillez à utiliser les méthodes fournies par votre langage/cadre de travail pour obtenir l'extension du fichier afin d'éviter des problèmes tels que l'injection d'octets nuls. 

Il peut également être tentant de valider le type de contenu du téléchargement, mais cela peut le rendre très fragile, étant donné que les types de contenu utilisés pour des fichiers spécifiques peuvent différer d'un système d'exploitation à l'autre. En outre, cela ne vous apprend rien sur le fichier lui-même, puisque le type de contenu n'est qu'un mappage à partir d'une extension. 

Problème 3 - Ne pas empêcher la traversée du chemin

Un autre problème commun aux téléchargements de fichiers est qu'ils ont tendance à être vulnérables au détournement de chemin. Il s'agit là d'un sujet à part entière, aussi, plutôt que d'essayer de le résumer ici, jetez un coup d'œil à l'intégralité des lignes directrices sur le Path Traversal.

Plus d'exemples

Vous trouverez ci-dessous quelques autres exemples de téléchargements de fichiers sécurisés et non sécurisés. 

C# - Insecure

public string UploadProfilePicture(IFormFile uploadedFile)
{
    // Generate path to save the uploaded file at
    var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";

    // Save the file
    var localFile = File.OpenWrite(path);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Update the profile picture
    UserProfile.UpdateUserProfilePicture(request.User, path)

    return path;
}

C# - Sécurisé

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

public string UploadProfilePicture(IFormFile uploadedFile)
{
    // NOTE: The best option is to avoid saving files to the local disk.
    var basePath = Path.GetFullPath("./uploads/avatars/");

    // Prevent path traversal by not utilizing the provided file name. Also needed to avoid filename conflicts.
    var newFileName = GenerateFileName(uploadedFile.FileName);

    // Generate path to save the uploaded file at
    var canonicalPath = Path.Combine(basePath, newFileName);

    // Ensure that we did not accidentally save to a folder outside of the base folder
    if(!canonicalPath.StartsWith(basePath))
    {
        return BadRequest("Attempted to save file outside of upload folder");
    }

    // Ensure only allowed extensions are saved
    if(!IsFileAllowedExtension(uploadedAllowedExtensions))
    {
        return BadRequest("Extension is not allowed");
    }

    // Save the file
    var localFile = File.OpenWrite(canonicalPath);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Update the profile picture
    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 - Insé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<>("Upload error", 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();
           // Make sure that the extension is an allowed type
            if(!IsAllowedExtension(file.getOriginalFilename()) {
                return new ResponseEntity<>("Extension not allowed", HttpStatus.FORBIDDEN);
            }

            // Make sure that the file is not saved outside of the upload root
           if(!uploadPath.toString().startsWith(baseFolder.toString()))            {
                return new ResponseEntity<>("Files are not allowed to be saved outside of the base folder.", HttpStatus.FORBIDDEN);
           }

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

        } catch (Exception e) {
            return new ResponseEntity<>("Upload error", 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 - Insecure

@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)))

# Assurez-vous que l'extension se trouve dans l'ensemble autorisé
if not is_extension_allowed(file.filename) :
return "This extension is not allowed"

# Assurez-vous que le fichier vers lequel nous essayons de sauvegarder ne se trouve pas en dehors de la base
if not savedFilePath.startsWith(baseFolder) :
return "Attempted to save file outside of base folder"

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")