Bezpieczeństwo aplikacji Laravel: CSRF, XSS, SQL injection i OWASP
Autor:
Paweł Matusiak
·
Włamania do stron i paneli nie biorą się z „pecha”. Biorą się z pominiętych zabezpieczeń. Laravel ma wbudowaną ochronę przed najczęstszymi atakami — pod warunkiem, że ktoś jej naprawdę używa.
Właściciel firmy zwykle nie pyta o token CSRF. Pyta, czy ktoś wyciągnie bazę klientów, czy konkurencja podejrzy cennik, i czy panel nie padnie w piątek wieczorem. Te pytania sprowadzają się do kilku klasycznych wektorów ataku — i właśnie przed nimi Laravel broni się z pudełka, jeśli aplikacja jest zbudowana porządnie. Framework nie jest amuletem. Źle skonfigurowany Laravel z włączonym debugiem na produkcji, sekretami w Git i wspólnym kontem biuro wycieknie tak samo jak stary skrypt PHP.
W GESOFT bezpieczeństwo nie jest osobną pozycją „jak zostanie budżet”. Jest listą kontrolną przy każdym panelu, który trzyma zamówienia, faktury albo dane osobowe. Poniżej: trzy ataki, które wciąż działają na tanie strony, pełna mapa OWASP Top 10 w wydaniu 2025 (aktualna lista na 2026 rok) i to, co dokładamy poza standardem frameworka. Jeśli masz już stary panel i nie wiesz, czy te rzeczy są zrobione — zacznij od audytu bezpieczeństwa PHP, nie od kolejnej funkcji.
Trzy ataki, które wciąż działają na tanie strony
SQL injection — kradzież danych przez formularz
Atakujący wkleja złośliwy fragment SQL w pole wyszukiwania, logowania albo w parametr URL. Źle napisana aplikacja skleja ten tekst z zapytaniem i wykonuje je na bazie. Skutek: wyciek tabeli klientów, obejście logowania, czasem skasowanie danych. W Laravelu Eloquent i Query Builder używają parametrów powiązanych (prepared statements przez PDO). Surowy SQL z formularza nie trafia do silnika jako kod. Surowych zapytań (DB::raw, whereRaw) używamy tylko wyjątkowo — i zawsze ze związanymi parametrami, nigdy ze sklejką stringów.
Walidacja requestów jest drugą linią: typ, długość, lista dozwolonych wartości. Nie ufamy temu, co przyszło z przeglądarki, nawet jeśli frontend Vue „nie pozwala” wpisać liter w pole liczby. API odrzuci sprytne wywołanie z innej przystawki. To samo dotyczy sortowania i filtrów — nazwa kolumny do ORDER BY nigdy nie pochodzi wprost z query stringa bez allow-listy.
XSS — złośliwy skrypt w przeglądarce użytkownika
Jeśli aplikacja wyświetla imię klienta, komentarz albo treść zgłoszenia bez escapowania, ktoś może wstawić skrypt, który kradnie sesję, podmienia numer konta na fakturze albo wysyła żądanie w imieniu zalogowanego handlowca. Blade, silnik szablonów Laravela, domyślnie escapuje zmienne w {{ }}. Surowe {!! !!} stosujemy wyłącznie do treści, którą sami wygenerowaliśmy. We Vue.js treść też jest escapowana, chyba że ktoś świadomie użyje v-html. Tego nie robimy na danych od użytkownika — ani w panelu, ani w mailach HTML składanych z pól formularza.
CSRF — akcja w Twoim imieniu
Użytkownik jest zalogowany. Otwiera inną stronę, która po cichu wysyła żądanie zmień hasło, usuń fakturę albo przelej środki do Twojej aplikacji. Przeglądarka dołącza ciasteczko sesji, bo to ten sam origin albo źle ustawione SameSite. Laravel wymaga tokenu CSRF przy formularzach i żądaniach zmieniających stan (POST, PUT, PATCH, DELETE) — bez niego serwer odrzuca request. W SPA na Sanctum (cookie) ten sam mechanizm działa razem z nagłówkiem X-XSRF-TOKEN; nie wystawiamy mutujących endpointów bez tej pary. GET nic nie zmienia — to stara, wciąż łamana zasada.
OWASP Top 10:2025 — checklista, nie marketing
OWASP Top 10 to standardowy dokument świadomości dla twórców aplikacji webowych. Wydanie 2025 (obowiązujące, gdy czytasz ten tekst w 2026) przestawiło akcenty: na szczycie nadal jest złamana kontrola dostępu, ale wysoko weszły zła konfiguracja i awarie łańcucha dostaw oprogramowania. W projektach GESOFT traktujemy listę jako listę kontrolną przy review i przy audycie, nie jako ozdobnik na stronie.
- A01 Broken Access Control — nadal numer jeden. Role i policies w Laravelu, testy uprawnień na CI, brak ukrytych URL-i jako jedynego zabezpieczenia. Jawny identyfikator w adresie (/faktury/1842) nie oznacza, że wolno ją zobaczyć.
- A02 Security Misconfiguration — skok z miejsca 5. (2021) na 2. Debug wyłączony na produkcji, osobne środowiska, minimalne uprawnienia użytkownika bazy, brak listowania katalogów, sekrety tylko w .env.
- A03 Software Supply Chain Failures — nowa, szeroka kategoria: zależności Composer/npm, pipeline CI, aktualizacje. Regularny composer audit, brak porzuconych paczek, lockfile w Git, sekrety poza repozytorium.
- A04 Cryptographic Failures — hasła przez bcrypt/argon2id, HTTPS wszędzie, żadnych haseł i kluczy API w kodzie ani w dumpie. Dane szczególnie wrażliwe szyfrujemy w spoczynku, gdy model tego wymaga.
- A05 Injection — ORM, walidacja, allow-listy. Nie tylko SQL: także komendy systemowe i nagłówki. Uploady plików sprawdzamy po zawartości, nie po rozszerzeniu, które przysłał klient.
- A06 Insecure Design — zagrożenia myślą się na analizie, nie po wdrożeniu. Wspólne konto biuro, brak retencji, eksport całej bazy jednym kliknięciem bez drugiego składnika — to błędy projektu, nie „zapomniany hotfix”.
- A07 Authentication Failures — limity logowań, 2FA tam gdzie stawka jest wysoka, wygasające sesje, reset hasła jednorazowym linkiem. Szerzej: uwierzytelnianie i 2FA w Laravelu.
- A08 Software or Data Integrity Failures — nie ufamy deserializacji z zewnątrz, nie wciągamy paczek z nieznanego źródła, nie odpalamy niezaufanego webhooka bez podpisu.
- A09 Security Logging and Alerting Failures — log audytowy (kto, kiedy, z jakiego IP) plus alert, nie tylko plik, którego nikt nie czyta. Hasła i PESEL nie lądują w logach.
- A10 Mishandling of Exceptional Conditions — nowa pozycja 2025. Błąd nie może wylać stack trace na produkcję ani zostawić przelewu w stanie nieokreślonym. Transakcje, idempotencja płatności, czytelny komunikat dla użytkownika bez szczegółów infrastruktury.
Co dokładamy poza standardem Laravela
- Rate limiting na logowanie, formularze i API — utrudnia zgadywanie haseł i spam. Osobno na IP i na konto, z narastającym czasem blokady, nie z wiecznym czekaniem.
- Nagłówki bezpieczeństwa: wymuszenie HTTPS i HSTS, ograniczenie iframe (clickjacking), Referrer-Policy, rozsądne CSP tam, gdzie panel na to pozwala.
- Logi audytowe: kto zmienił dane wrażliwe, kiedy, z jakiego IP. Retencja logów ustalona z góry, bez haseł i pełnych identyfikatorów w treści.
- Kopie zapasowe bazy i plików poza serwerem produkcyjnym oraz testowane odtwarzanie. Backup, którego nikt nie sprawdził, to nie backup.
- Dwuskładnikowe logowanie tam, gdzie panel ma dostęp do pieniędzy albo danych osobowych — obowiązkowe dla admina i księgowości.
- Hardening serwera: firewall, SSH tylko na klucz, aktualizacje systemu, osobny użytkownik bazy z minimalnymi prawami, katalog .env poza dokument root.
Łańcuch dostaw (A03) w praktyce oznacza: lockfile w repozytorium, aktualizacje po CVE, brak leftover-skryptów w public/, brak zapomnianych paneli phpMyAdmin. Konfiguracja (A02) oznacza osobny .env na produkcję, APP_DEBUG=false, i nigdy nie commitujemy .env do Gita. Te dwa punkty z listy 2025 zamykają więcej realnych incydentów w MŚP niż egzotyczny atak kryptograficzny.
Panel, API i błędy, które widać za późno
Ukrycie przycisku w menu Vue nie jest kontrolą dostępu. Policy w Laravelu musi odrzucić request, nawet gdy ktoś ręcznie wywoła /api/place/eksport. Test automatyczny „handlowiec nie pobierze raportu płac” ma być zielony na CI. To A01 w praktyce — i najczęstszy błąd, który znajdujemy w audytach starych paneli.
Wyjątki (A10) są niedoceniane, dopóki bramka płatności zwróci timeout, a system wystawi fakturę dwa razy albo wcale. Projektujemy idempotencję: ten sam webhook nie księguje drugiej wpłaty. Użytkownik widzi spróbuj ponownie, nie zrzut klasy PDOException. Na produkcji logujemy szczegóły po stronie serwera, nie w odpowiedzi HTTP.
RODO i bezpieczeństwo nachodzą na siebie: szyfrowanie w transporcie, minimalne uprawnienia, zgłaszanie naruszeń. Samo „mamy SSL” nie zamyka art. 32. Jak projektujemy zgody, retencję i prawo do usunięcia — w RODO w aplikacjach webowych. Nowy projekt na Laravelu dostaje te mechanizmy od pierwszego dnia, razem z Git i 6 miesiącami gwarancji.
Powierzchnia ataku to nie tylko kod aplikacji i serwer. To też ludzie: wspólne hasło w mailu, stażysta z prawem admina na tydzień, który zostaje na zawsze, reset hasła na skrzynkę, której nikt nie czyta. Szkolenie zespołu nie zastąpi policies, ale policies nie zastąpią zakazu wysyłania haseł na Slacku. Łączymy jedno z drugim: twarde limity w aplikacji i prosta instrukcja dla właściciela, kto może zapraszać użytkowników i jak wygląda offboarding, gdy handlowiec odchodzi. Bez tego 2FA na koncie, którego nikt nie wyłącza po odejściu pracownika, jest tylko ozdobą na slajdzie z audytu, nie prawdziwą tarczą.
Hosting i wdrożenie są częścią tej samej listy. Katalog public/ nie może serwować .env, .git ani backupów SQL. PHP na wersji bez supportu (7.2, 7.4 po EOL) to otwarte drzwi niezależnie od jakości kodu biznesowego. HTTPS z przekierowaniem z HTTP, certyfikat odnawiany automatycznie, SSH tylko kluczem — to nuda, która zatrzymuje masowe skany. W WordPressie powierzchnia ataku rośnie z każdą wtyczką; w Laravelu rośnie z każdą nieprzemyślaną zależnością i z każdym endpointem bez policy. Skala jest inna, nawyk ten sam: nie dokładaj tego, czego nie utrzymasz.
Jeśli masz już aplikację PHP i nie jesteś pewien, czy te rzeczy są zrobione — napisz, co chcesz zabezpieczyć. Powiemy konkretnie, od czego zacząć: szybki przegląd, pełny audyt albo plan naprawy, zanim ktoś inny znajdzie tę samą dziurę. Nowy projekt dostaje te mechanizmy od razu, z Git i 6 miesiącami gwarancji, bez pozycji bezpieczeństwo jako opcjonalny dodatek na końcu oferty.
Mass assignment, uploady i to, czego ORM sam nie zamknie
Eloquent chroni przed SQL injection przy zwykłym where/create. Nie chroni przed masowym przypisaniem: request->all() wciągnięty w Model::create bez $fillable albo z $guarded = [] ustawia is_admin=1, jeśli ktoś doda pole w JSON. W projektach GESOFT modele mają jawną listę pól, a Form Request — osobną listę dozwolonych kluczy. To A01 i A05 naraz: złamana kontrola dostępu plus injection logiki, nie tylko SQL. To samo przy sortowaniu: nazwa kolumny ORDER BY nigdy nie pochodzi wprost z query stringa.
Upload to klasyka RCE na tanich panelach: plik.php.jpg, podwójne rozszerzenie, MIME z klienta, skrypt w /public/uploads. Sprawdzamy zawartość, nie deklarację. Pliki lądują poza document root albo za jednorazowym, podpisanym URL. Miniatury i PDF robi worker, nie request użytkownika z Imagick na niezaufanym blobie (historia CVE ImageMagick / Ghostscript jest długa). Limit rozmiaru i liczby plików na endpoint — rate limit nie dotyczy tylko logowania.
Nagłówki, ciasteczka i konfiguracja, która wycieka sama
A02 2025 (Security Misconfiguration) wyprzedziło injection, bo debug, listowanie katalogów, domyślne hasła i CORS „*” są częstsze niż ręcznie sklejany SQL. Na produkcji: APP_DEBUG=false, HTTPS z HSTS, cookie sesji Secure + HttpOnly + SameSite=Lax (Strict tam, gdzie panel nie wychodzi w linkach z maila), SERVER nie zdradza wersji PHP. CORS tylko dla znanych originów SPA. phpinfo() i Telescope nie istnieją na produkcji. .env, .git i storage/logs nie są serwowane z public/.
Sekrety: APP_KEY, hasło bazy, klucze bramek — tylko środowisko, rotacja po odejściu admina i po incydencie. APP_KEY w Git jest równoznaczny z możliwością odszyfrowania cookie i encrypted casts. composer.lock commitujemy; composer audit w CI. Porzucona paczka (ostatni commit trzy lata temu, brak PHP 8) wylatuje albo zostaje zastąpiona. To A03 łańcucha dostaw — ta sama klasa ryzyka co wtyczka WordPressa, tylko na mniejszej powierzchni.
IDOR, BOLA i test, którego nie widać w menu
Ukryty przycisk w Vue nie jest kontrolą dostępu. /api/invoices/1842 z ID zgadniętym albo z kolejnego numeru to BOLA/IDOR, wprost pod A01. Policy Laravel (InvoicePolicy@view) musi odrzucić request, nawet gdy token jest ważny. Test automatyczny: user A nie czyta faktury usera B; handlowiec nie eksportuje płac; gość nie woła /api/admin. To odpalamy na CI, nie „na oko po kliknięciu”. W audycie starych paneli ten punkt pada najczęściej — częściej niż złamany bcrypt.
A10 2025 (Mishandling of Exceptional Conditions) to timeout bramki, podwójny webhook, pełny dysk w trakcie uploadu, worker, który umarł w połowie joba. Transakcja bazy, idempotency key płatności, retry z backoffem, martwy list queue z alertem. Użytkownik widzi „spróbuj ponownie”, nie stack PDOException. Log serwerowy ma request-id, nie hasło i nie PESEL. RODO i bezpieczeństwo schodzą się tutaj: wyciek w logu jest naruszeniem, nawet gdy SQL injection nie było.
Najczęściej zadawane pytania
- Czy Laravel jest bezpieczny z pudełka?
- Ma ochronę CSRF, XSS i SQL injection, jeśli używasz ORM, szablonów i walidacji. Bezpieczeństwo to też konfiguracja, aktualizacje, uprawnienia i łańcuch dostaw — framework ich nie włączy sam. Debug na produkcji i sekrety w Git znoszą resztę zalet.
- Co to jest OWASP Top 10 2025?
- Aktualna lista najgroźniejszych ryzyk w aplikacjach webowych. W 2025 na szczycie są: złamana kontrola dostępu, zła konfiguracja, awarie łańcucha dostaw, kryptografia, injection, niepewny projekt, błędy uwierzytelniania, integralność, logowanie/alerty oraz zła obsługa wyjątków.
- Czy stary panel PHP da się zabezpieczyć bez przepisywania?
- Często tak: aktualizacja PHP, limity logowań, HTTPS, kopie, nagłówki. Rewrite, gdy framework jest porzucony albo nikt nie doda funkcji bez ryzyka. Zaczynamy od audytu, nie od slajdu przepisać wszystko.
- Ile kosztuje audyt bezpieczeństwa?
- Zależnie od wielkości kodu i czy sprawdzamy serwer. Typowy panel — raport w kilka dni roboczych, wycena po oględzinach zakresu. Nie ma jednej ceny katalogowej za audyt. Napisz, jaki system chcesz sprawdzić.
- Czy potrzebuję 2FA w każdym panelu?
- Dla administratorów i księgowości — tak. Dla kont o niskich uprawnieniach bywa opcjonalne. Dobieramy TOTP albo SMS do ryzyka, nie do mody. Szczegóły w artykule o 2FA.
- Co z wtyczkami i paczkami Composer?
- Każda zależność to powierzchnia ataku (A03). Trzymamy lockfile, aktualizujemy po CVE, nie dokładamy porzuconych paczek. To inna skala niż WordPress z 40 wtyczkami z katalogu, ale ignorowanie composer audit kończy się tak samo.
- Czy SSL wystarczy na bezpieczeństwo i RODO?
- Nie. HTTPS zamyka transport. Zostają uprawnienia, sekrety, kopie, retencja, logi i łańcuch dostaw. SSL jest konieczne i niewystarczające.
- Czy po wdrożeniu dostaję kod i gwarancję?
- Tak — repozytorium Git, dokumentacja i 6 miesięcy gwarancji na wykonane prace. Aktualizacje bezpieczeństwa po tym okresie można objąć pakietem opieki.
Powiązana usługa:
Aplikacje Laravel i Vue.js
Opisz projekt