• Czas czytania ~2 min
  • 25.06.2022

Głównym celem programisty jest zdobycie zaufania naszych użytkowników. Chcemy, aby zaufali naszemu kodowi, naszym aplikacjom i naszej marce. Bo jeśli użytkownik nam zaufa, będzie wracał. Zaakceptują i będą żyć z denerwującymi błędami i brakującymi funkcjami (nie, że tego chcemy, ale się zdarzają...) i będą ufać, że wszystko inne będzie działać, że ich dane będą bezpieczne, a my nie marnujemy ich czasu.

Jednak jako programista absolutnie nie możemy ufać naszym użytkownikom. Kiedykolwiek!

Kiedy mówimy o zaufaniu użytkownikom, oznacza to, że nie możemy ufać ich wkładowi. Nie możemy im ufać, że podadzą prawidłowe informacje, użyją poprawnych formatów, a nawet wykonają tylko oczekiwane czynności.Problem polega na tym, że użytkownicy są złożonymi i frustrującymi stworzeniami. Niektórzy użytkownicy mają złośliwe intencje i będą szukać sposobów na złamanie lub złamanie zabezpieczeń Twoich aplikacji. Inni mogą myśleć inaczej niż ty i zrobią rzeczy, których się nie spodziewasz - natkną się na błędy i uzyskają dostęp do danych, do których nie powinni mieć dostępu (moja żona jest w tym ekspertem!).A inni są po prostu zagubieni i zdezorientowani, i będą próbować wrócić do miejsca, w którym się rozpoczęli.

Więc naszym zadaniem jako programistów jest zabezpieczenie naszych aplikacji przed naszymi użytkownikami. Musimy być paranoiczni z otrzymywanymi danymi wejściowymi i mieć wiele zabezpieczeń, aby zapewnić, że bez względu na to, co robi użytkownik, nasza aplikacja jest bezpieczna. Aha, i jakoś przekonaj naszych użytkowników, że im nie ufamy, żeby nam zaufali.Łatwe, prawda?

Więc dzisiaj przyjrzymy się kilku sposobom, w jakie możemy uniknąć zaufania naszym użytkownikom:

Weryfikowanie danych wejściowych

Kiedy myślisz o „danych wejściowych” do swojej aplikacji, od razu myślisz o formularzach i ich danych. Prosisz użytkowników o podanie pewnych informacji lub odpowiedzi na niektóre pytania, a następnie otrzymujesz i przechowujesz dane, które podali.

Rozważ następujący formularz:

Forma podstawowa

Spośród zebranych tutaj fragmentów informacji bardzo łatwo byłoby założyć, że adres e-mail będzie prawidłowym adresem e-mail (zwłaszcza jeśli użyliśmy pola wejściowego adresu e-mail HTML5), a kraj będzie prawidłowym krajem (ponieważ jest pole wyboru).

Ale nie możesz ufać swoim użytkownikom, przyjmując te założenia. Użytkownik może zmodyfikować kod HTML w przeglądarce, aby te pola wejściowe akceptowały absolutnie wszystko.

Zamiast tego musimy jasno określić naszą walidację danych wejściowych:

  • First / last name
    • Required field
    • Must be a string
    • Max length 255
  • Email address
    • Required field
    • Must be a string
    • Must be a recognisable email address format
    • Max length 255
    • Must not be used by another user in the database
  • Country
    • Required field
    • Must be in a pre-defined list of allowed countries

Teraz, gdy mamy już zdefiniowane wyraźne zasady, możemy użyć niesamowicie potężnego komponent sprawdzania poprawności do sprawdzania poprawności danych wejściowych, aby upewnić się, że jest dokładnie tym, czego oczekujemy.

$data = $request->validate([
    'first_name' => ['required', 'string', 'max:255'],
    'Last_name'  => ['required', 'string', 'max:255'],
    'email'      => ['required', 'string', 'email', 'max:255', 'unique:users'],
    'country'    => ['required', Rule::in($this->allowedCountries())],
]);

Jeśli walidator przejdzie pomyślnie, wiemy, że $data zawiera wartości zgodne z naszymi oczekiwaniami, które można bezpiecznie przechowywać w bazie danych i ostrożnie używać w całej aplikacji.Kraj jest bezpieczny w użyciu, ponieważ będzie dokładnie zgodny z naszą listą dozwolonych i wiemy, że e-mail jest w prawidłowym formacie, abyśmy mogli zacząć wysyłać do niego powiadomienia.

Imię/nazwisko, z drugiej strony, przejdziemy do następnego.

Zanim przejdziemy dalej, należy zauważyć, że walidator zwróci tylko klucze, które zostały uwzględnione w regułach walidacji. Oznacza to, że wszelkie dodatkowe dane, które zostaną przesłane w formularzu, zostaną zignorowane, dzięki czemu można bezpiecznie przejść bezpośrednio do modelu za pomocą create(), fill() lub update(), unikając tak zwanej luki w zabezpieczeniach przypisań masowych. W ten sposób zawsze przechowuję dane w moich modelach.

Sparametryzowane zapytania

Wdrożyliśmy więc nasze formularze i poprosiliśmy naszych użytkowników o dane, które podali. Ale teraz musimy napisać kilka zapytań do bazy danych, aby wykorzystać nasze dane. Nadchodzi znowu ten nieznośny problem zaufania.

W Laravel zwykle piszemy nasze zapytania w ten sposób:

$name = $request->query(‘name');
$user = DB::table('users')->where('name', $name)->get();

Kluczowym składnikiem jest tutaj metoda where(), gdzie drugi parametr ($name) pochodzi bezpośrednio z danych wejściowych użytkownika. Laravel automatycznie dołącza ten drugi parametr jako sparametryzowane zapytanie, co zapobiega wstrzykiwaniu SQL poprzez przekazanie go bezpośrednio do bazy danych.Jest to jedna z genialnych funkcji Laravela, która utrudnia niezamierzone pisanie podatnych zapytań.

A co, jeśli potrzebujesz naprawdę złożonego zapytania lub chcesz użyć logiki specyficznej dla silnika bazy danych? Lub jeśli potrzebujesz po prostu uruchomić zapytanie, które jest znacznie wydajniejsze do pisania w surowym SQL?

Przekonasz się, że czasami trzeba zagłębić się w zapytania niestandardowe, i to wtedy, gdy zapytania parametryczne są ważne.

Rozważ ten kod:

public function store(Request $request)
{
    $data = $request->validate([
        'game' => ['required'],
        'date' => ['required'],
    ])
 
    DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = {$data['game']}");
}

Kiedy programista pisał ten kod, oczekiwał, że $data['game'] będzie liczbą całkowitą po wstrzyknięciu do zapytania (i przeczytali te surowe porównania liczb całkowitych są szybsze!).Jasne, przeszedł przez przeglądarkę, ale doszli do wniosku, że to ukryte pole i nikt by tego nie zauważył! Ale nadal jest to pole wejściowe, a haker może je dowolnie modyfikować...

Jak wspomniałem wcześniej, Laravel zawiera parametryzację wbudowaną w konstruktora zapytań i możemy z łatwością użyć tego również podczas tworzenia zapytań ręcznych.Zamiast wstrzykiwać zmienne bezpośrednio do ciągu zapytania, zastąp je znakiem zapytania (?) i uwzględnij je jako drugi parametr w wywołaniu metody. Baza danych rozumie, że znak zapytania jest symbolem zastępczym i wie, jak bezpiecznie zastąpić parametry w zapytaniu podczas jego wykonywania.

W tym przypadku możemy zrobić coś takiego:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = ?", [$data['game']]);

Zasadniczo wszystkie metody zapytań Laravela obsługują parametry w ten sposób, więc nie ma wymówki, aby ich nie używać.

(Na marginesie, możemy również naprawić tę konkretną lukę poprzez walidację danych wejściowych liczb całkowitych, a najlepiej zrobilibyśmy oba, aby zapewnić dodatkową ochronę.)

Jeśli chcesz zagłębić się w sparametryzowane zapytania, napisałem o nich na Laravel Security in Depth, polecam sprawdzić oficjalną Bazę danych i Eloquent dokumentacja.

Zanim przejdziemy dalej, przyjrzyjmy się szybko, co haker mógł zrobić z tym podatnym zapytaniem.

Aby dodać kontekst: kiedy to zapytanie jest wykonywane, zwiększa liczbę tur w grze dla każdego użytkownika o wartość event_increment. Przynosi to jednakowe korzyści wszystkim użytkownikom w grze.

Gdybym był hakerem, który odkrył tę lukę, chciałbym zwiększyć tylko moje własne tury w grze. Mógłbym użyć serii zgadnięć i prób i błędów, aby wymyślić dane wejściowe, które wyglądają tak, aby przesłać je w polu gra:

42 && user_id = (SELECT id FROM users WHERE email = '[email protected]' LIMIT 1)

Przesłanie tego do aplikacji spowodowałoby powstanie następującego zapytania SQL:

DB::statement("UPDATE game_user SET turns += event_increment WHERE game_id = 42 && user_id = (SELECT id FROM users WHERE email = `[email protected]` LIMIT 1)");

Nieco posprzątane:

UPDATE game_user
SET turns += event_increment
WHERE game_id = 42
  AND user_id = (
    SELECT id
    FROM users
    WHERE email = '[email protected]'
    LIMIT 1
  )

I to wszystko.Moje obroty będą się zwiększać, ale nikt inny nie. Super proste, wszystko z powodu braku walidacji i parametryzacji zapytań.

To trywialny przykład, ale możesz zrobić DUŻO z SQL Injection. Istnieje wiele sztuczek do wydobywania informacji, na przykład gdy strona nie zwraca żadnych widocznych informacji zwrotnych (lub komunikatów o błędach!).Jeśli chcesz dowiedzieć się więcej, zagłębiliśmy się w SQL Injection w Laravel Security in Depth, w tym celowo podatną na ataki aplikację internetową, w której możesz przeprowadzać własne ataki SQLi. Sprawdź to tutaj.

Uciekanie wyjść

W tym momencie prawdopodobnie chcesz, żebym się pospieszył i skończył, abyś mógł wrócić i sprawdzić, czy wszystkie twoje walidacje są wystarczająco wyraźne, a twoje zapytania są odpowiednio sparametryzowane, więc nie będę cię dłużej zatrzymywać. (Wiem, że za każdym razem, gdy piszę o tych rzeczach, mam okropne retrospekcje do strasznie wrażliwego kodu, który napisałem w przeszłości!)

Ale jest jedna rzecz, którą chcę podkreślić, zanim skończymy tutaj, a jest to prawidłowe unikanie danych. Jeśli pamiętasz jedną rzecz, pamiętaj, że powinieneś zrobić wszystko, aby uniknąć używania nieuniknionych tagów ostrzy.

Te rzeczy tutaj:

{!! $variable !!}

Proszę, po prostu ich nie używaj.

Musisz uważać na to, co wyświetlasz na stronie.Rozważ nasze dane wprowadzone wcześniej, z imieniem i nazwiskiem użytkownika.

Co, jeśli użytkownik przesłał to jako swoje imię:

Stephen <script src="https://evilhacker.dev/evil.js"></script>

A potem zrobiliśmy to na stronie:

{{ $user->first_name }}

Jak można się było spodziewać, zobaczylibyśmy ich nazwę i tag skryptu – wydrukowane zwykłym tekstem. Załadowanie byłoby całkowicie bezpieczne i od razu wiedzielibyśmy, że próbują wstrzyknąć jakiś zły javascript.

A gdyby nasz kod wyglądał tak:

// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">{$user->first_name}</a>")
    ->join(', ', ' and ');
 
return view('pages', ['links' => $links])
// Template
<div>
    {!! $links !!}
</div>

Wszystko, co zobaczymy, to „Stephen”, a złośliwy Javascript byłby uruchomiony w przeglądarce, robiąc wszystko, czego chce haker. Możesz łatwo zobaczyć, dlaczego programista sięgnął po to rozwiązanie, ale pozostawia aplikację szeroko otwartą na XSS.

Więc jak to robimy?

  1. We have the e($value) function, which does the actual escaping of output when you use the {{ $value }} tags. You can call it anywhere.
  2. If you wrap something inside the Illuminate\Support\HtmlString class, it will bypass escaping.
// Controller
$links = $pages
    ->map(fn ($page) => "<a href=\"{$page->url}\">".e($user->first_name)."</a>")
    ->join(', ', ' and ');
 
return view('pages', ['links' => new HtmlString($links)])
// Template
<div>
    {{ $links }}
</div>

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297