Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

Ataki i zabezpieczenia na upload plików.

utworzone przez Java, RestAPI, Security, Spring Framework

Przyszło mi ostatnio testować API, które służyło do odczytu danych z dokumentów typu dowód osobisty. API na wejściu przyjmowało plik ze zdjęciem dokumentu i parę innych danych procesowych – był więc to czysty upload plików. Jako rezultat pracy endpintu dostawaliśmy JSON z danymi z OCR z dokumentu (dane odczytane z obrazu do postaci tekstowej). Podczas moich testów, byłem w stanie wysłać dowolny plik na serwer – np. virus.exe. Konsekwencje trzymania danych poufnych i podatności w uploadzie są chyba oczywiste 🙂

Z tego artykułu dowiesz się

  • Jak prawidłowo walidować upload plików?
  • Jak zabezpieczyć się przed uploadem pliku exe?
  • Czym są magic bytes i jak je wykorzystać w walidacji uploadu?

Niezabezpieczony upload plików

Aby zrozumieć problem, stwórzmy proste RestAPI służące do uploadu plików. Na razie nie będziemy zabezpieczać naszej końcówki Rest w żaden sposób. Na poniższym kodzie widzimy prosty Rest Controller i prostą metodę służącą do uploadu plików.

@RestController
@RequestMapping(path = "/cards")
@RequiredArgsConstructor
public class CardController {

    private final AddCardImageService addCardImageService;

    @PutMapping(value = "/{cardId}/uploadImage", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity addCardImage(@PathVariable UUID cardId,
                                       @RequestParam("file") MultipartFile multipartFile) {
        addCardImageService.addImage(cardId, file);
        return ResponseEntity.ok().build();
    }
}

W naszych testach skorzystamy z narzędzia Postman i Burp. Postmanem będziemy wysyłać zapytania do naszej aplikacji. Burp posłuży nam jako proxy, dzięki któremu będziemy mogli przechwytywać i edytować zapytania wysyłane do serwera.

Na poniższym screenie widzimy przechwycony request. Jak widzimy wysyłamy plik o nazwie „card.png”

Poniżej widać odpowiedź serwera. Wszystko poszło w porządku. Plik zapisał się na serwerze. Usługa zwróciła status 200 OK.

Spróbujmy teraz zaatakować nasze API. W tym celu podmieńmy nasz plik card.png na virus.exe

Jak widzimy na powyższym screenie z programu burp – w zapytaniu zmieniła się nazwa pliku wraz z jego rozszerzeniem oraz Content-Type – czyli nagłówek mówiący o typie wysyłanego pliku. W oryginale wartość nagłówka Content-Type była równa „image/png”, teraz mamy „application/x-msdos-program”. Aplikacja, jako że nie posiada żadnego zabezpieczenia na uploadzie plików – nie jest w stanie zwalidować danych wejściowych. A co za tym idzie – przesłany plik jest przekazywany dalej. Mamy dziurę!

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Dodajmy teraz klasę walidującą dane wejściowe w naszym RestAPI. Będziemy od tej pory odrzucać potencjalnie złośliwe żądania. W pierwszej kolejności będziemy badać nazwę uploadowanego pliku – a konkretniej jego rozszerzenie. W kontrolerze dodaliśmy odwołanie do walidacji pliku wejściowgo.

@RestController
@RequestMapping(path = "/cards")
@RequiredArgsConstructor
public class CardController {

    private final AddCardImageService addCardImageService;

    @PutMapping(value = "/{cardId}/uploadImage", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity addCardImage(@PathVariable UUID cardId,
                                       @RequestParam("file") MultipartFile multipartFile) {

        final byte[] file = UploadFileValidator.validateMultipartFile(multipartFile);
        addCardImageService.addImage(cardId, file);
        return ResponseEntity.ok().build();
    }
}

Plik UploadFileValidator.java reprezentuje walidację plików z RestAPI.

public class UploadFileValidator {

    private static final String PNG = "png";
    private static final String JPG = "jpg";

    @SneakyThrows
    public static byte[] validateMultipartFile(final MultipartFile multipartFile) {
        validateFileExtension(multipartFile);
        return multipartFile.getBytes();
    }

    private static void validateFileExtension(MultipartFile multipartFile) {
        final String fileExtenstion = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
        if (!fileExtenstion.equals(JPG)
                && !fileExtenstion.equals(PNG)) {
            throw new InvalidFileTypeException();
        }
    }
}

Spróbujmy przetestować poprawkę i zaatakujmy RestAPI. Po ponownym wykonaniu zapytania z dołączonym plikiem virus.exe dostaniemy błędną odpowiedź serwera. Nasza poprawka zadziałała!

Podmieńmy obraz na plik virus.exe ale dodatkowo zmieńmy nazwę pliku na virus.exe.png

Jak widzimy na zdjęciu poniżej – nasza walidacja przepuściła plik virus.exe.png ! Mało tego, niektórzy rozbijają ciąg znaków tworzący pełną nazwę wraz z rozszerzeniem po pierwszej kropce i to, co zostanie po kropce, traktują jako rozszerzenie (sprawdzając przy tym, czy ciąg rozszerzenia zawiera kluczowe słowo typu jpg czy png) ! Taka walidacja powinna znaleźć się w naszym kodzie, ale nie może to być jedyna walidacja pliku wejściowego!

Czym dysponujemy oprócz nazwy i rozszerzenia pliku? Jest to oczywiście Content-Type, mówiący nam o tym, z jakim typem pliku mamy do czynienia – niezależnie od jego nazwy czy rozszerzenia! Brzmi świetnie? Sprawdźmy to! Dodajmy do naszego walidatora, walidację po Content-Type pliku, akceptując tylko image/png oraz image/jpg

public class UploadFileValidator {

    private static final String PNG = "png";
    private static final String JPG = "jpg";
    private static final String CONTENT_TYPE_PNG = "image/png";
    private static final String CONTENT_TYPE_JPG = "image/jpg";

    @SneakyThrows
    public static byte[] validateMultipartFile(final MultipartFile multipartFile) {
        validateContentType(multipartFile);
        validateFileExtension(multipartFile);

        return multipartFile.getBytes();
    }

    private static void validateFileExtension(MultipartFile multipartFile) {
        final String fileExtenstion = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
        if (!fileExtenstion.equals(JPG)
                && !fileExtenstion.equals(PNG)) {
            throw new InvalidFileTypeException();
        }
    }

    private static void validateContentType(MultipartFile multipartFile) {
        if (!multipartFile.getContentType().equals(CONTENT_TYPE_JPG)
                && !multipartFile.getContentType().equals(CONTENT_TYPE_PNG)) {
            throw new InvalidFileTypeException();
        }
    }
}

Wyślijmy jeszcze raz nasze żądanie do serwera.

Jak widzimy na poniższej odpowiedzi – nasza walidacja zadziałała! Jest coraz lepiej – nie pozwalamy na upload pliku z rozszerzeniem innym niż png, jpg oraz nie pozwalamy na Content-Type inny niż image/png, oraz image/jpg!

Czy to wystarczy? Przetestujmy ponownie nasze RestAPI z użyciem burp. W tym narzędziu jesteśmy w stanie przechwycić żądanie i je dowolnie zmodyfikować. Co się stanie, jeśli ręcznie zmodyfikujemy nagłówek Content-Type? Zależy nam na uploadzie wirusa, ale chcemy przejść walidację. Do dzieła!

Jak widać na zdjęciu powyżej, zmodyfikowaliśmy nagłówek Content-Type na image/png. Nie ma to żadnego wpływu na zawartość pliku. Nasza walidacja przepuściła wirusa na serwer! Jak więc zabezpieczyć upload plików?

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Magic bytes – jako zabezpieczenie uploadu plików

Magic bytes, czyli magiczne bajty to początek każdego pliku, który zawiera podstawowe informacje – takie jak typ pliku. Jeśli atakujący zmodyfikuje magic bytes odpowiedzialne za typ pliku i np. dla pliku virus.exe ustawi magic bytes, tak aby mówiły, że jest to plik typu png – plik zostanie uszkodzony – w efekcie czego po kliknięciu plik, system operacyjny zgłosi błąd. Wirus będzie więc zupełnie niegroźny.

Na poniższym screenie z burp widać zawartość całego żądania HTTP, w tym uploadowanego pliku. Jak widzimy, występuje tam słowo PNG (przy uploadzie pliku typu PNG).

Przy uploadzie pliku virus.exe – magic byte nie wskazują, żeby to był obraz png.

W systemie rodziny Windows magic bytes możemy sprawdzić za pomocą programu HxD Hex Editor. Po otwarciu pliku exe w edytorze HEX widzimy początkowe bajty, określające typ pliku – w przypadku pliku binarnego DOS Executable początkowe dwa bajty to 0x4D oraz 0x5A.

Z kolei dla pliku o typie PNG, początkowe bajty to kolejno: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A, prezentuje to screen poniżej.

Poniższa tabela prezentuje sygnatury magic byte dla różnych typów plików.

W prezentowanym przykładzie interesują nas wyłącznie pliki obrazu. Zakładamy, że nasza white lista typów plików przyjmie tylko pliki PNG i JPG. W dodatku nie musimy sprawdzać wszystkich ośmiu bajtów, żeby stwierdzić, czy mamy do czynienia z typem pliku, który chcemy przepuścić – wystarczy sprawdzenie dwóch pierwszych bajtów.

  • Dla pliku PNG – dwa pierwsze bajty w zapisie szesnastkowym to kolejno 0x89 i 0x50
  • Dla pliku JPG – dwa pierwsze bajty w zapisie szesnastkowym to kolejno 0xFF i 0xD8

Napiszmy więc funkcję zwracającą typ pliku na podstawie magic bytes.

private static FileType getFileType(final byte[] fileData) {
        if (Byte.toUnsignedInt(fileData[0]) == 0x89 && Byte.toUnsignedInt(fileData[1]) == 0x50)
            return FileType.PNG;
        else if (Byte.toUnsignedInt(fileData[0]) == 0xFF && Byte.toUnsignedInt(fileData[1]) == 0xD8)
            return FileType.JPG;
        throw new UnsuportedFileType();
    }

Przy okazji możemy dodać walidację rozmiaru pliku, tak aby ktoś nam nie uploadował zdjęcia wielkości 1Gb 🙂 Cały kod walidatora może wyglądać następująco.:

public class UploadFileValidator {

    private static final String PNG = "png";
    private static final String JPG = "jpg";
    private static final String CONTENT_TYPE_PNG = "image/png";
    private static final String CONTENT_TYPE_JPG = "image/jpg";
    private static final long MAX_FILE_SIZE = 1024l * 1024l * 10l;

    @SneakyThrows
    public static byte[] validateMultipartFile(final MultipartFile multipartFile) {
        validateFileType(multipartFile);
        validateContentType(multipartFile);
        validateFileExtension(multipartFile);
        validateFileSize(multipartFile);

        return multipartFile.getBytes();
    }

    private static void validateFileExtension(MultipartFile multipartFile) {
        final String fileExtenstion = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
        if (!fileExtenstion.equals(JPG)
                && !fileExtenstion.equals(PNG)) {
            throw new InvalidFileTypeException();
        }
    }

    private static void validateContentType(MultipartFile multipartFile) {
        if (!multipartFile.getContentType().equals(CONTENT_TYPE_JPG)
                && !multipartFile.getContentType().equals(CONTENT_TYPE_PNG)) {
            throw new InvalidFileTypeException();
        }
    }

    private static void validateFileSize(MultipartFile multipartFile) {
        if (multipartFile.getSize() > MAX_FILE_SIZE) {
            throw new FileSizeNotAllowedException();
        }
    }

    private static void validateFileType(MultipartFile multipartFile) throws IOException {
        final FileType fileType = getFileType(multipartFile.getBytes());

        if (!fileType.equals(FileType.PNG) && !fileType.equals(FileType.JPG)) {
            throw new InvalidFileTypeException();
        }
    }

private static FileType getFileType(final byte[] fileData) {
        if (Byte.toUnsignedInt(fileData[0]) == 0x89 && Byte.toUnsignedInt(fileData[1]) == 0x50)
            return FileType.PNG;
        else if (Byte.toUnsignedInt(fileData[0]) == 0xFF && Byte.toUnsignedInt(fileData[1]) == 0xD8)
            return FileType.JPG;
        throw new UnsuportedFileType();
    }
}

Upload wysokiego ryzyka

W aplikacjach takich jak bankowe czy telekomunikacyjne często udostępniana jest możliwość podgrywania plików tj. dokumenty do kredytu, skany dowodu osobistego czy inne. Nie trudno sobie wyobrazić potencjalne skutki uploadu wirusa w połączeniu z siecią bankową. Aby zapobiec takim sytuacjom, często stosuje się dodatkowe zaawansowane walidacje takie jak:

  • Walidacje wymienione w artykule.
  • Kontrola dostępu do API uploadu (wymaganie zalogowania).
  • Sprzętowy firewall.
  • Logowanie incydentów bezpieczeństwa.
  • Podpięcie skanerów antywirusowych do API uploadu.
  • Banowanie klientów, którzy naruszyli reguły bezpieczeństwa.
  • Uruchamianie uploadowanego pliku w środowisku kontrolowanym (tzw. sanbox) i sprawdzanie interakcji z systemem operacyjnym.

Połączenie wyżej wymienionych technik walidacji uploadowanego pliku daje nam niemal 100% pewności bezpieczeństwa API uploadu. Jednak tak zaawansowane techniki, jak skanowanie antywirusowe czy sanbox są wykorzystywane w krytycznych aplikacjach.

Zakończenie

Jak więc widzisz drogi czytelniku przy uploadzie plików można popełnić kilka krytycznych podatności. Już wiesz jak się zabezpieczyć, mocno walidując dane wejściowe. Przy zabezpieczeniu samego RestAPI uprawnieniami czy logowaniem, mamy bardzo silne Endpointy, które będą bardzo trudne do złamania na testach penetracyjnych, czy już na środowisku produkcyjnym. Jeśli podobał Ci się wpis – koniecznie podziel się nim ze swoimi znajomymi. Dopisz się również na nasz newsletter, jeśli jeszcze tego nie zrobiłeś, a nie ominie Cię żaden mięsny artykuł.

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Gdybyś potrzebował jeszcze więcej:

Jesteś Java Developerem?

Przejdź na wyższy poziom wiedzy 
„Droga do Seniora” 🔥💪

Jesteś Team Leaderem? Masz zespół?

Podnieś efektywność i wiedzę swojego zespołu 👌

Podsumowanie

Co to jest magic bytes?

Magic bytes to kilka początkowych bajtów pliku, dzięki którym możemy dowiedzieć się, jaki jest typ pliku niezależnie od jego rozszerzenia. Stosuje się to m.in. przy walidacji uploadu danych na serwer.

Jak zabezpieczyć upload plików?

Powinniśmy stworzyć white listę akceptowalnych typów plików, a sam typ badać na podstawie rozszerzenia i magic byte pliku. Dodatkowo można walidować maksymalny rozmiar, czy nazwę pliku.

Jak atakować upload plików?

Atakujący może przechwycić żądania za pomocą proxy np. BURP, gdzie może nadpisać żądanie HTTP i wysłać dowolny plik na niezabezpieczony upload w RestAPI.

Zdjęcie: Biznes zdjęcie utworzone przez d3images – pl.freepik.com

Dyskusja