Leitlinien

Hochladen von Dateien

Es kommt häufig vor, dass Anwendungen es Benutzern ermöglichen müssen, eine Datei (entweder zur Verwendung oder nur zur Speicherung) irgendwo innerhalb der Anwendung hochzuladen. Auch wenn es einfach erscheint, kann die Implementierung dieser Funktion aufgrund der potenziellen Risiken, die mit der Handhabung von Datei-Uploads verbunden sind, ziemlich kritisch sein. 

Schauen Sie sich dieses kurze Beispiel an, damit Sie besser verstehen, was wir meinen. 

Nehmen wir an, es handelt sich um eine Anwendung, die es den Nutzern ermöglicht, ein Profilbild hochzuladen:

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;
}

Dies wäre eine sehr einfache Upload-Funktion, die auch anfällig für Path Traversal ist. 

Je nach der genauen Implementierung der Anwendung könnte ein Angreifer eine andere Seite/ein anderes Skript hochladen (z. B. .asp-, .aspx- oder .php-Dateien), die einen direkten Aufruf und die Ausführung von beliebigem Code ermöglichen würden. Dies könnte auch das Überschreiben bestehender Dateien ermöglichen. 

Problem 1 - Speichern auf der lokalen Festplatte statt in einem externen Datenspeicher

Mit der zunehmenden Nutzung von Cloud-Diensten werden Anwendungen in Containern bereitgestellt, Hochverfügbarkeitskonfigurationen sind zum Standard geworden, und die Praxis, hochgeladene Dateien auf die lokale Festplatte der Anwendung zu schreiben, sollte um jeden Preis vermieden werden. 

Die Dateien sollten nach Möglichkeit in einen zentralen Speicher hochgeladen werden (Blockspeicher oder Datenbank). Dadurch können in diesem Fall ganze Klassen von Sicherheitslücken vermieden werden. 

Problem 2 - Nicht validierte Erweiterungen 

In vielen Fällen, in denen eine Sicherheitslücke beim Hochladen von Dateien ausgenutzt wird, hängt dies von der Möglichkeit ab, eine Datei mit einer bestimmten Erweiterung hochzuladen. Daher ist es sehr ratsam, eine "Zulassen-Liste" von Erweiterungen für Dateien zu verwenden, die hochgeladen werden können. 

Stellen Sie sicher, dass Sie die von Ihrer Sprache/Framework bereitgestellten Methoden verwenden, um die Dateierweiterung zu ermitteln, um Probleme wie Null-Byte-Injektion zu vermeiden. 

Es mag auch verlockend sein, den Inhaltstyp des Uploads zu validieren, aber das kann die Sache sehr unsicher machen, da sich die für bestimmte Dateien verwendeten Inhaltstypen von Betriebssystem zu Betriebssystem unterscheiden können. Außerdem sagt es nichts über die Datei selbst aus, da der Inhaltstyp lediglich eine Zuordnung aus einer Erweiterung ist. 

Problem 3 - Pfadüberquerung nicht verhindert

Ein weiteres häufiges Problem bei Datei-Uploads ist, dass sie auch anfällig für Path Traversal sind. Das ist ein ganz eigenes Thema. Anstatt zu versuchen, es hier zusammenzufassen, sollten Sie sich den vollständigen Leitfaden zu Path Traversal ansehen.

Weitere Beispiele

Nachfolgend finden Sie einige weitere Beispiele für sichere und unsichere Datei-Uploads, die Sie sich ansehen können. 

C# - Unsicher

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# - Sicher

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 - Unsicher

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

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

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

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

# Sicherstellen, dass die Erweiterung im erlaubten Satz ist
if not is_extension_allowed(file.filename):
return "Diese Erweiterung ist nicht erlaubt"

# Sicherstellen, dass die Datei, in der wir versuchen zu speichern, nicht außerhalb der Basis liegt
if not savedFilePath.startsWith(baseFolder):
return "Es wurde versucht, eine Datei außerhalb des Basisordners zu speichern"

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