Jak zabezpieczyć aplikację webową, której nie rozumiesz do końca

Gdzieś tam jest aplikacja webowa, za którą odpowiadasz, i nikt nie potrafi Ci z poważną miną powiedzieć, czy formularz logowania można bezpiecznie wystawić do internetu. Może aplikację odziedziczyłeś. Może kolega naklepał ją przez weekend. Może ma dziesięć lat, a pierwotnego zespołu dawno nie ma. Wszystko to jest tym samym problemem: musisz zabezpieczyć aplikację webową, której wnętrza nie rozumiesz do końca. Nie możesz przejść przez każdą trasę, każde sprawdzenie uwierzytelnienia, każdą zależność i każde zapytanie do bazy i osobiście za nie zaręczyć. Przepisanie zajmuje kwartały, których nie masz, a zostawienie tego wystawionego jest nie do przyjęcia. O tym środku właśnie jest ten artykuł.

Jeśli potraktujemy aplikację, którą mamy, jako czarną skrzynkę, musimy bronić jej z zewnątrz, warstwa po warstwie. Żadne jedno narzędzie nie zabezpieczy całości, ale garść komplementarnych, ułożonych jedno na drugim, pokrywa większość oczywistych sposobów, w jaki zostanie przejęta. Trzy podstawowe warstwy, z grubsza w kolejności, w jakiej należy je dodawać:

WarstwaCo robi
Skanowanie sekretówZnajduje poświadczenia, które wyciekły do repozytorium i jego historii.
Filtrowanie na brzeguWAF przed aplikacją, filtrujący jawnie złośliwy ruch.
Ochrona przed botamiCAPTCHA albo detekcja botów na podstawie wyniku na formularzach, które muszą pozostać publiczne.

Wszystkie trzy warstwy powyżej siedzą na brzegu — granicy między Twoją aplikacją a światem zewnętrznym. Skanowanie sekretów nie pozwala poświadczeniom wyciekać z repozytorium, WAF filtruje wchodzący złośliwy ruch, ochrona przed botami rzuca wyzwanie nieludzkim odwiedzającym na formularzach, które muszą pozostać publiczne. Żadna z nich nie potrzebuje rozumieć wnętrza aplikacji, by działać — i właśnie dlatego są tak użyteczne, gdy to wnętrze jest niepewne.

OWASP ma na ten wzorzec nazwę: wirtualne łatanie — warstwy ochronne, które zmniejszają ekspozycję, gdy leżący pod nimi kod jest jeszcze niepewny. Żadna z nich nie zastępuje porządnego przepisania ani porządnego audytu. Wszystkie kupują Ci czas.

Brzeg to miejsce, w którym ten artykuł spędza większość czasu, ale omówimy też trochę tego, co można zrobić za brzegiem — patrząc zamiast tego w środek: na biblioteki, które aplikacja faktycznie uruchamia, na jej kod źródłowy i na jej zachowanie w czasie działania. Skanowanie zależności znajduje biblioteki ze znanymi lukami, które aplikacja już ze sobą wozi, a SAST i DAST na końcu artykułu wyłapują wzorce na poziomie kodu, których nie widzi żadna warstwa brzegowa.

Większość tych warstw ma solidną opcję do samodzielnego hostowania, działającą na dowolnym hoście — gitleaks do sekretów, ModSecurity do WAF-a, OSV-Scanner do zależności, Semgrep do SAST, ZAP do DAST. Te pozostają wiodące przez cały tekst. Co do zarządzanych odpowiedników w chmurze — gdzie wymieniasz część pracy konfiguracyjnej na dostrojony przez dostawcę zestaw reguł i jego wiedzę o zagrożeniach — swoje ma każda duża chmura. Jako konkretny punkt odniesienia, gdy zarządzany przykład pomaga, sięgniemy po Google Cloud Platform: Cloud Armor do WAF-a, Artifact Analysis do skanowania kontenerów, reCAPTCHA Enterprise i Adaptive Protection do obrony przed botami. Wybór jednej chmury czyni te przykłady konkretnymi, a nie machnięciem ręką; AWS, Azure i Cloudflare mają odpowiedniki, o których wspomnimy krótko, ale nie zejdziemy w nie głęboko.

Trzy kategorie nieznanego

Przed narzędziami warto nazwać, przed czym właściwie się bronisz, bo to kształtuje, która warstwa ma największe znaczenie. Nieznane rozpada się na trzy grupy.

Co jest w kodzie i w repozytorium. Nie wiesz, które trasy istnieją — aplikacje odziedziczone rosną organicznie i są tam prawdopodobnie endpointy administracyjne, o których nikt nie wspomniał. Nie wiesz też, co kod właściwie robi, bo albo nie zdążysz go przeczytać (legacy), albo nikt go za pierwszym razem uważnie nie czytał (wygenerowany przez AI). I nie wiesz, co już wyciekło do historii gita. DAST sonduje działającą aplikację z zewnątrz, by zmapować to, co jest faktycznie wystawione; SAST daje niepełną, ale użyteczną mapę ryzykownych wzorców kodu; skanowanie sekretów wyłapuje poświadczenia zapomniane w starych commitach.

Co działa obok kodu. Nie wiesz, które zależności są aktualne — dziesięcioletnia aplikacja ma dziesiątki bibliotek z publicznymi CVE (Common Vulnerabilities and Exposures — publiczny katalog znanych luk bezpieczeństwa, każda z ustandaryzowanym identyfikatorem w rodzaju CVE-2024-12345). Skanowanie zależności to najbardziej opłacalna warstwa dla tego przypadku.

Co dzieje się z aplikacją na produkcji. Nie wiesz, kto Cię atakuje i jak — dziennik audytowy WAF-a powie Ci to w ciągu doby od uruchomienia. I nie wiesz, jaka część Twojego ruchu to boty, co na publicznych formularzach rejestracji, logowania i resetu hasła często stanowi większość. Ochrona przed botami to pokazuje i pozwala na to zareagować.

Żadne narzędzie nie rozwiązuje wszystkich tych rzeczy. Każda z trzech warstw poniżej adresuje jedną albo więcej z nich; skanowanie zależności (omówione po warstwach) i SAST/DAST (na końcu) pchają głębokość dalej, gdy podstawy są już na miejscu.

Skanowanie sekretów

Repozytoria gubią poświadczenia — odziedziczone, bo rosły organicznie latami, naklepane, bo LLM-y radośnie wpisują klucze na twardo „dla wygody”, a diffa nikt nie przejrzał. Klucze API, klucze dostępu AWS, hasła do baz, tokeny usług wewnętrznych — kończą w commitach, w plikach konfiguracyjnych, w starych fixture’ach testowych, w dokumentacji, w wygenerowanych przez AI przykładach, które po cichu podstawiają prawdziwy klucz zamiast wypełniacza. A ponieważ git trzyma historię na zawsze, nawet commity, które „usunęły” sekret, wciąż go zawierają.

Najszybszy zwrot z aplikacji-czarnej-skrzynki daje przeskanowanie pełnej historii gita każdego zaangażowanego repozytorium, rotacja wszystkiego, co się znajdzie, a potem utrzymywanie skanera w CI, by nowe wycieki były wyłapywane.

Narzędzia

  • gitleaks — szybki, w Go, ze znakomitymi domyślnymi regułami pokrywającymi większość dostawców (AWS, GCP, Azure, Stripe, Twilio, tokeny GitHuba, klucze prywatne, JWT). Uruchamiaj go na pełnej historii, nie tylko na HEAD.
  • trufflehog — podobna detekcja, ale dodaje krok weryfikacji: sprawdza, czy wykryty sekret jest faktycznie żywy (np. wykonuje wywołanie sts:GetCallerIdentity dla kluczy AWS). Wyjście o wyższym sygnale, dodatkowe opóźnienie tego warte.
  • git-secrets — nastawiony na AWS, lekki, przydatny jako hook pre-commit.
  • GitHub Secret Scanning — darmowy dla repozytoriów publicznych, Advanced Security dla prywatnych. Współpracuje z dostawcami, więc wyciekłe tokeny są automatycznie unieważniane.

Uruchamiamy skan i czytamy wyjście

gitleaks da się zainstalować na kilka sposobów — gotowy binarny plik, Homebrew, go install albo przez Dockera. Użyjemy tu Dockera dla wygody: nic nie trzeba instalować na hoście, ta sama komenda działa na dowolnej maszynie z demonem Dockera, a runnery CI dostają to samo wywołanie co Twój laptop.

Właśnie uruchomiłem to na repozytorium, w którym żyje ten artykuł:

docker run --rm -v "$(pwd):/repo" zricethezav/gitleaks:latest \
    detect --source=/repo --log-opts="--all" --redact --verbose

--log-opts="--all" przechodzi przez każdy commit na każdej gałęzi, nie tylko HEAD; --redact maskuje wartość sekretu w wyjściu, żeby sam raport nie stał się nowym wyciekiem; --verbose wypisuje pełny blok dla każdego znaleziska, a nie tylko zbiorczy licznik.

gitleaks emituje jedno znalezisko na każdy potencjalny łańcuch-sekret, który rozpozna — ten sam zestaw pól dla każdego dopasowania, niezależnie od tego, która reguła odpaliła. Typowe znalezisko wygląda tak:

Finding:     STRIPE_SECRET_KEY=REDACTED
Secret:      REDACTED
RuleID:      stripe-access-token
Entropy:     4.175736
File:        blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx
Line:        225
Commit:      49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa
Fingerprint: 49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa:blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx:stripe-access-token:225

Pięć pól, które mają znaczenie:

PoleCo Ci mówi
RuleIDKtóra reguła detekcji odpaliła (stripe-access-token, aws-access-token, generic-api-key, private-key itd.). Pełny domyślny zestaw reguł leży w config/gitleaks.toml w repozytorium gitleaks — każda reguła ze swoim regexem, opisem i wszelkimi filtrami słów kluczowych.
File + LineGdzie jest dopasowanie.
CommitKtóry commit je wprowadził — nie musi być ten najnowszy. gitleaks znajduje je tam, gdzie po raz pierwszy pojawia się w historii.
EntropyEntropia Shannona dopasowanego łańcucha, w bitach na znak. Wyżej = wygląda bardziej losowo, co zwykle znaczy, że bardziej prawdopodobnie jest prawdziwym sekretem. Reguły dla ogólnych kluczy API używają entropii jako głównego sygnału; reguły specyficzne dla dostawcy (Stripe, AWS) dopasowują prefiks i jej nie potrzebują.
FingerprintStabilny identyfikator, którym stłumisz to znalezisko, jeśli okaże się dopuszczalne. Więcej o tym niżej.

Co entropia właściwie mierzy

Entropia Shannona mierzy, jak nieprzewidywalny jest średnio każdy znak łańcucha. Łańcuch, w którym każdy znak z równym prawdopodobieństwem mógł być którąkolwiek z N wartości, ma maksymalną możliwą entropię: log₂(N) bitów na znak. Łańcuch, w którym jeden znak pojawia się znacznie częściej niż inne albo w którym następny znak da się przewidzieć z poprzedniego, ma entropię niższą.

Dla skanowania sekretów pytanie, na które entropia próbuje odpowiedzieć, brzmi: czy ten łańcuch wygląda, jakby wygenerowało go źródło losowe, czy człowiek? Prawdziwe sekrety — klucze API, hashe, tokeny w base64 — są projektowane, by być nieprzewidywalne, więc pchają entropię ku maksimum alfabetu. Imiona, angielskie słowa, daty, łańcuchy wersji, ścieżki plików i szablonowa treść siedzą znacznie niżej. Entropia to tani test, który oddziela „to wylosował CSPRNG” od „to wklepał człowiek albo wygenerował kod według wzorca”.

Zgrubna skala:

  • poniżej ~3.0 — wygląda jak tekst angielski, identyfikatory, łańcuchy wersji albo powtarzalna struktura. Rzeczy w rodzaju password123, [email protected], v1.2.3-beta, ścieżki plików.
  • ~4.0 — blisko maksimum dla czystego heksa (log₂(16) = 4). Hashe MD5/SHA, tokeny w heksie, UUID-y.
  • ~5.0 — typowo dla losowych tokenów alfanumerycznych. Klucze API ze stałym prefiksem i wysokoentropijnym losowym ogonem.
  • ~5.95 — pułap dla całkowicie losowego 62-znakowego alfabetu [a-zA-Z0-9]. Długie sekrety w stylu base64 albo czysto alfanumeryczne.

Nasze znalezisko powyżej dostało 4.175736 — wygodnie powyżej domyślnego progu gitleaks ≈3.5 dla reguł ogólnych i dokładnie w pasmie, w które wpadają prawdziwe klucze dostawców. Stały prefiks (coś w rodzaju sk_live_) trochę ściąga średnią entropię w dół; losowy ogon pcha ją z powrotem w górę. Ta kombinacja to dokładnie to, jak wygląda prawdziwy klucz API.

Triaż znalezisk

Gdy naprawdę uruchomisz gitleaks na prawdziwym repozytorium, dostaniesz znaleziska trzech kategorii — a ta sama reakcja nie pasuje do wszystkich trzech:

  1. Prawdziwy wyciek. Poświadczenie zacommitowane przez przypadek — plik .env, plik konfiguracyjny z kluczem API na twardo, fixture testowy z prawdziwym tokenem. Rotuj natychmiast. Przyjmij, że poświadczenie jest skompromitowane, nawet jeśli repozytorium jest prywatne. Byli kontraktorzy i stare kopie zapasowe laptopów nie są teoretyczne. Potem zdecyduj, czy przepisywać historię (tylko dla przypadków wysokiej wrażliwości, jak produkcyjne hasła do baz albo klucze podpisujące — przepisywanie jest destrukcyjne, rób to świadomie).
  2. Zamierzone włączenie. Łańcuch faktycznie dopasowuje się do wzorca sekretu, ale ma być w repozytorium: dokumentacja cytująca poświadczenia kontrolowane przez atakującego jako dowód w analizie bezpieczeństwa, fixture’y testowe z celowo fałszywymi, ale realistycznie wyglądającymi wartościami, przykładowe pliki konfiguracyjne. Repozytorium, w którym żyje ten artykuł, ma dokładnie taki przypadek — analiza ataku na łańcuch dostaw cytuje wpisane na twardo w złośliwym projekcie klucze Stripe i API, by pokazać, czym posługiwał się atakujący. gitleaks słusznie je flaguje, ale to nie są prawdziwe poświadczenia, więc nie ma czego rotować.
  3. Fałszywy alarm. Wzorzec się dopasował, ale łańcuch nie jest w rzeczywistości sekretem — losowy base64 w hashu, długa wartość w rodzaju UUID, słowo z komunikatu commita o wysokiej entropii. Rzadsze przy regułach specyficznych dla dostawcy, częstsze przy ogólnych.

Dla kategorii 2 i 3 lekarstwem jest plik .gitleaksignore w korzeniu repozytorium, wyliczający identyfikatory — pole Fingerprint z tabeli powyżej — znalezisk, które przejrzałeś i zatwierdziłeś:

# Zamierzone: sekrety cytowane jako dowód w analizie ataku na łańcuch dostaw
49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa:blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx:stripe-access-token:225
49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa:blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx:generic-api-key:193

gitleaks będzie pomijać dokładnie te znaleziska przy kolejnych skanach. Komentuj hojnie — przyszły Ty musi wiedzieć, czy każdy wpis został zatwierdzony, bo jest zamierzony, czy bo jest fałszywym alarmem, a komentarz to jedyny trwały zapis tej decyzji.

Zatrzymaj następny wyciek w momencie commita

Gdy skan historyczny jest czysty (albo odciski są pobrane i stłumione), wepnij gitleaks w:

  • CI — blokuje build na jakichkolwiek nowych znaleziskach. To samo wywołanie Dockera działa w dowolnym runnerze CI.
  • Hook pre-commit — wyłapuje wycieki, zanim w ogóle dotrą do Twojego lokalnego repozytorium. Jest do tego oficjalny tryb gitleaks protect, albo użyj frameworku pre-commit z hookiem gitleaks.

Razem znaczą one, że nowy wyciek wymaga teraz aktywnego obejścia obu warstw — o wiele trudniej zrobić to przez przypadek niż celowo.

Filtrowanie na brzegu z WAF-em

Web Application Firewall to odwrotne proxy, które inspekcjonuje żądania HTTP(S) względem zestawu reguł. Decyduje, czy każde żądanie przepuścić, zalogować, zablokować, czy rzucić mu wyzwanie, zanim dotrze do Twojej aplikacji. WAF jest użyteczny do:

  • Wyłapywania oczywistych ładunków dla klas ataków: wstrzyknięcie SQL, XSS, lokalne/zdalne włączanie plików, wstrzyknięcie komend, przechodzenie po ścieżkach.
  • Blokowania znanych złych ścieżek (/wp-admin/, .git/config, .env).
  • Ograniczania tempa, blokowania po reputacji IP, filtrowania geograficznego.
  • Kupowania czasu, gdy w zależności, której nie możesz dziś podnieść, wychodzi CVE.

Nie jest użyteczny przy zepsutej logice biznesowej, zepsutej autoryzacji albo złym projekcie sesji. To środek kompensujący, nie lekarstwo.

Do WAF-a prowadzą dwie ścieżki operacyjne. Zarządzany WAF prowadzi Twój dostawca chmury; samodzielnie hostowany WAF to taki, który prowadzisz sam. Zestaw reguł detekcji pod oboma jest w istocie ten sam — otwartoźródłowy silnik ModSecurity v3 w parze z OWASP Core Rule Set (CRS), albo przemianowana przez dostawcę wersja tego samego. CRS to około 200 utrzymywanych społecznościowo reguł detekcji pokrywających wstrzyknięcia SQL, XSS, RCE, przechodzenie po ścieżkach i resztę OWASP Top 10. Używa punktacji anomalii — reguły dodają do wyniku, a żądanie jest blokowane tylko wtedy, gdy suma przekroczy próg — co czyni dostrajanie łagodniejszym niż blokowanie per reguła.

Więc wybór między zarządzanym a samodzielnym jest głównie operacyjny, a nie o jakości detekcji. Obie ścieżki uruchamiają w istocie ten sam silnik i te same reguły.

Zarządzany: Cloud Armor

Zarządzany WAF jest niemal zawsze właściwym punktem startu: mniej infrastruktury do prowadzenia, szybsze wdrożenie (popołudnie, nie tydzień), brak silnika do aktualizowania i dostrajania. Wybierz to najpierw, chyba że masz konkretny powód, by tego nie robić.

Natywną opcją GCP jest Google Cloud Armor. Przyczepia się do load balancera HTTP(S) w GCP, a jego prekonfigurowane grupy reguł (sqli-v33-stable, xss-v33-stable, lfi-v33-stable, rce-v33-stable itd.) wywodzą się wprost z OWASP CRS — tego samego zestawu reguł, który zainstalowałbyś ręcznie z ModSecurity.

Cloud Armor można konfigurować przez konsolę GCP, CLI gcloud, REST API albo — co pokażemy poniżej — deklaratywnie w Terraformie. Model jest ten sam niezależnie od interfejsu: polityka bezpieczeństwa Cloud Armor to lista reguł z warunkami dopasowania i akcjami (allow, deny(403), rate_based_ban). Minimalna polityka włączająca prekonfigurowane grupy reguł CRS i sensowną akcję domyślną wygląda tak:

resource "google_compute_security_policy" "app" {
  name = "app-waf"

  rule {
    action   = "deny(403)"
    priority = 1000
    match {
      expr { expression = "evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 2})" }
    }
    description = "Block SQL injection"
  }

  rule {
    action   = "deny(403)"
    priority = 1001
    match {
      expr { expression = "evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 2})" }
    }
    description = "Block XSS"
  }

  rule {
    action   = "allow"
    priority = 2147483647
    match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } }
    description = "Default allow"
  }
}

Jest jeden tryb wart wczesnej wzmianki: tryb podglądu Cloud Armor. Gdy jest włączony, reguły są wyliczane normalnie i logują każde dopasowanie do Cloud Logging — ale faktycznie nie odrzucają ruchu. To zarządzany odpowiednik uruchomienia ModSecurity w trybie tylko-detekcji i tak właśnie bezpiecznie włączasz zupełnie nową politykę na prawdziwym ruchu produkcyjnym bez ryzyka przypadkowego zablokowania swojego callbacku płatności o trzeciej nad ranem. Przebieg wdrożenia jest ten sam co dla wersji samodzielnej: tydzień w podglądzie, dostrajanie na podstawie tego, co odpala, potem wyprowadzasz reguły z podglądu po jednej.

Poza regułami WAF-a Cloud Armor dołącza albo łączy się z kilkoma pokrewnymi zdolnościami: ochrona przed DDoS (zawsze włączona), warstwa ML adaptacyjnej ochrony wykrywająca anomalie wolumetryczne i automatycznie sugerująca reguły, oraz zarządzanie botami. Dwie ostatnie to płatne dodatki, a nie darmowe funkcje bazowe.

Kompromisy, o których warto wiedzieć. Język reguł własnych Cloud Armor to podzbiór CEL — mniej wyrazisty niż pełna składnia SecRule w ModSecurity, więc bardzo zawiłych reguł własnych czasem nie da się wyrazić bezpośrednio. Wersję CRS wybiera Google, nie Ty, więc nie zostaniesz na konkretnej rewizji. I jest koszt za żądanie na wierzchu wychodzącego ruchu LB.

Inni dostawcy chmury mają swoje odpowiedniki. AWS WAF przyczepia się do ALB/CloudFront/API Gateway z własną składnią reguł plus zarządzanymi grupami reguł; Azure Front Door / Application Gateway WAF też jest oparty na CRS; Cloudflare WAF jest architektonicznie inny — to odwrotne proxy CDN, a nie filtr przyczepiony do LB, więc kierujesz DNS na Cloudflare, a Twój origin chowa się za ich siecią. Wszystkie trzy zawierają zarządzane grupy reguł oparte na CRS i odpowiednik trybu tylko-detekcji.

Samodzielnie: ModSecurity + NGINX

Ścieżka samodzielna jest właściwym wyborem, gdy chcesz pełnej kontroli nad regułami własnymi, nie jesteś w chmurze z dobrą ofertą zarządzaną, koszt na żądanie ma znaczenie przy Twoich wolumenach albo ograniczenia zgodności czynią urządzenie, które prowadzisz sam, łatwiejszym do uzasadnienia niż usługę dostawcy.

Uruchamiasz ModSecurity v3 bezpośrednio jako dynamiczny moduł NGINX-a, skonfigurowany regułami OWASP CRS. Kształt architektury jest taki:

Internet  →  NGINX + ModSecurity  →  aplikacja w górę strumienia

Oto jak ułożone są te trzy części:

  • NGINX to odwrotne proxy. Terminuje TLS, przyjmuje wchodzące żądania HTTP i — gdy nic ich nie blokuje — proxuje je do aplikacji w górę strumienia na prywatnym porcie.
  • libmodsecurity to silnik wyliczania reguł. To biblioteka w C++, odrębna od samego NGINX-a, bez własnego kodu sieciowego — po prostu bierze żądanie na wejściu i zwraca werdykt.
  • Konektor ModSecurity dla NGINX-a to moduł dynamiczny (plik .so), który spina te dwa. Wczepia się w potok obsługi żądań NGINX-a i dla każdego wchodzącego żądania przekazuje URL, nagłówki i ciało do libmodsecurity na wyliczenie.

Wszystkie trzy części działają wewnątrz tego samego procesu NGINX. Mechanizmem jest ładowacz modułów dynamicznych NGINX-a — i konektor, i libmodsecurity kończą wczytane do każdego workera NGINX-a przy starcie, bez odrębnego demona ModSecurity, bez IPC, bez gniazda między nimi.

Gdy przychodzi żądanie, NGINX woła konektor, ten woła libmodsecurity — wszystko w obrębie wspólnej pamięci procesu. Dlatego integracja dodaje tylko kilka milisekund opóźnienia, a nie koszt obiegu do pozaprocesowej bramki bezpieczeństwa. I dlatego też wersja NGINX-a modułu-konektora musi dokładnie odpowiadać wersji NGINX-a, do której jest wczytywany — konektor jest kompilowany względem wewnętrznego ABI NGINX-a, które nie jest stabilne między wersjami.

Dla każdego żądania NGINX przekazuje URL, nagłówki i ciało do libmodsecurity (przez konektor). libmodsecurity przepuszcza je przez wczytane reguły — OWASP CRS plus wszelkie własne — kumuluje punktację anomalii i zwraca akcję: allow, log albo deny. Jeśli werdykt to deny, NGINX zwraca skonfigurowaną odpowiedź błędu (zwykle 403) i nigdy nie przekazuje w górę strumienia. W przeciwnym razie żądanie idzie normalnie.

Gdy WAF jest na miejscu, upewnij się, że wszystko to jest prawdą:

  • Aplikacja nie może być dostępna bezpośrednio. Jeśli nadal ma publiczny IP, atakujący obchodzą WAF-a w całości.
  • TLS terminuje się na WAF-ie. ModSecurity nie może inspekcjonować tego, czego nie może odszyfrować.
  • WAF jest teraz pojedynczym punktem awarii — uruchom co najmniej dwie instancje za LB, jeśli dostępność ma znaczenie.
  • Wolumen dziennika audytowego skoczy. Zaplanuj magazyn.

Kształt instalacji na Debianie/Ubuntu (skrótowo):

# libmodsecurity
git clone --depth 1 -b v3/master https://github.com/owasp-modsecurity/ModSecurity
cd ModSecurity && git submodule init && git submodule update
./build.sh && ./configure && make -j$(nproc) && make install

# moduł-konektor dla NGINX-a (musi odpowiadać Twojej wersji NGINX-a)
git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity-nginx
cd nginx-${NGINX_VERSION}
./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
make modules && cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/

# OWASP Core Rule Set
git clone --depth 1 https://github.com/coreruleset/coreruleset /etc/nginx/modsec/crs
cp /etc/nginx/modsec/crs/crs-setup.conf.example /etc/nginx/modsec/crs/crs-setup.conf

Następnie w nginx.conf:

load_module modules/ngx_http_modsecurity_module.so;

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    server {
        listen 443 ssl;
        server_name legacy-app.example.com;
        ssl_certificate     /etc/ssl/certs/app.crt;
        ssl_certificate_key /etc/ssl/private/app.key;

        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Wdrażaj najpierw w trybie tylko-detekcji. Ustaw SecRuleEngine DetectionOnly na co najmniej tydzień. Czytaj dziennik audytowy. Szukasz fałszywych alarmów (reguły CRS odpalające na legalnym ruchu — edytory, panele administracyjne, wysyłanie plików) i nieoczekiwanych prawdziwych trafień (ataki, które już Cię dotykają). CRS używa punktacji anomalii — reguły dodają wynik, a żądanie jest blokowane tylko wtedy, gdy suma przekroczy próg — co czyni dostrajanie łagodniejszym niż blokowanie per reguła.

Gdy znajdziesz fałszywy alarm, użyj najpierw SecRuleRemoveById zawężonego do konkretnego location, w drugiej kolejności obniż poziom paranoi na ścieżce, a wyłącz regułę globalnie tylko w ostateczności. Dopiero gdy dzienniki są dość ciche, by być sensowne, przełącz SecRuleEngine On.

Dla własnych łat wirtualnych — powiedzmy wiesz, że /api/report?format=X jest podatny na wstrzyknięcie komend — możesz odrzucać na brzegu wszystko poza listą dozwolonych:

SecRule REQUEST_URI "@beginsWith /api/report" \
    "id:1000001,phase:1,chain,deny,status:400,\
     msg:'virtual patch: /api/report format allowlist'"
    SecRule ARGS:format "!@rx ^(pdf|csv|json)$"

Ochrona przed botami i CAPTCHA

Pozostałe warstwy tego zestawu ledwie dotykają klasy zagrożeń, która niemal na pewno dosięgnie publiczną aplikację odziedziczoną: oportunistyczne nadużycia botów na endpointach, które muszą być nieuwierzytelnione — spamowe rejestracje, przebiegi upychania poświadczeń na Twoim formularzu logowania, masowe zgarnianie, zautomatyzowane nadużycia kuponów, zalewy resetów hasła wywołujące maile na skalę.

Własne uwierzytelnianie aplikacji tu nie pomaga — formularze rejestracji, logowania i resetu hasła muszą być osiągalne dla każdego. Ograniczanie tempa w WAF-ie pomaga, ale rozproszony botnet robiący po jednym uprzejmie wyglądającym żądaniu na IP wygląda identycznie jak fala legalnych rejestracji. Warstwą, która faktycznie odróżnia ludzi od botów, jest system detekcji botów albo CAPTCHA.

Ochrona przed botami to raczej spektrum niż jedno narzędzie:

  1. Pasywne klasyfikatory — modele ML oceniające każde żądanie na podstawie sygnałów (odcisk TLS, kolejność nagłówków, czasy, telemetria behawioralna) bez widocznego dla użytkownika wyzwania. Najmniejsze tarcie, działa najlepiej przeciw nadużyciom wolumetrycznym.
  2. CAPTCHA na podstawie wyniku — klasyfikator plus eskalacja do widocznego wyzwania tylko wtedy, gdy wynik jest niski. Hybryda, której używa większość nowoczesnych systemów.
  3. Zawsze widoczna CAPTCHA — klasyczne „kliknij wszystkie sygnalizatory”. Wysokie tarcie, dziś zwykle zły domyślny wybór, wciąż użyteczne dla konkretnych endpointów o wysokiej wartości.

Trzy podsekcje poniżej pokrywają dwie z tych kategorii spektrum — CAPTCHA na podstawie wyniku (Turnstile, reCAPTCHA Enterprise) i klasyfikator pasywny (Cloud Armor Adaptive Protection, Cloudflare Bot Management). Są raczej komplementarne niż alternatywne:

PodsekcjaKategoria spektrumPo stronie klienta?Najlepsze do
Cloudflare TurnstileCAPTCHA po wynikuTak — widget JS na każdym formularzuŁatwe zwycięstwa na formularzach rejestracji/logowania
reCAPTCHA EnterpriseCAPTCHA po wynikuTak — widget JS na każdym formularzuGłębsza integracja z GCP, egzekucja na brzegu przez Cloud Armor
Tylko klasyfikatorKlasyfikator pasywnyNie — wyłącznie sygnały serweroweNadużycia wolumetryczne, ruch, do którego nie możesz albo nie chcesz dodawać widgetu

Definiującą różnicą jest to, czy musisz dodać widget. Turnstile i reCAPTCHA Enterprise oba wymagają snippetu JS na każdym formularzu — widget zbiera sygnały poziomu przeglądarki (czasy, telemetrię behawioralną, wyzwania JS) i wysyła token, który Twój serwer weryfikuje. Narzędzia tylko-klasyfikator wyliczają każde żądanie, używając wyłącznie tego, co sieć i tak widzi: odcisku TLS, kolejności nagłówków HTTP, reputacji IP, anomalii wolumetrycznych. Nic do wbudowania, żadnej zmiany w kodzie aplikacji. Układają się naturalnie: klasyfikator pokrywa cały wchodzący ruch jako warstwa-kołdra; CAPTCHA celuje konkretnie w formularze o wysokiej wartości.

Zaczniemy od najprostszego w ustawieniu — Cloudflare Turnstile — a potem spojrzymy na głębszą, natywną dla GCP kombinację reCAPTCHA Enterprise plus Cloud Armor Adaptive Protection dla zespołów, które potrzebują więcej.

Cloudflare Turnstile (najprostsza droga)

Jeśli zrobisz dziś tylko jedną rzecz, niech to będzie ta. Cloudflare Turnstile jest darmowy, niewidoczny domyślnie, przyjazny prywatności (bez behawioralnego odciskania palców, jak robi to reCAPTCHA) i zgodny „na wstawkę” z API reCAPTCHA v2 — więc możesz później zmigrować bez przepisywania kodu formularzy, jeśli z niego wyrośniesz.

Ustawienie to szczerze praca na popołudnie i nie musisz hostować resztę aplikacji na Cloudflare — szczegóły konfiguracji znajdziesz w dokumentacji startowej Turnstile. Ustawienie nie wymaga żadnej zmiany DNS, Terraforma ani integracji z Twoim WAF-em.

Turnstile przestaje wystarczać, gdy potrzebujesz drobniejszej kontroli — wyników per żądanie, na które możesz zareagować (a nie tylko przeszło/nie przeszło), egzekucji na load balancerze zamiast w kodzie aplikacji albo reguł subtelniejszych niż „wyzwanie zaliczone / niezaliczone”. To właśnie tę lukę zapełnia kombinacja reCAPTCHA Enterprise + Cloud Armor poniżej.

reCAPTCHA Enterprise (głębsza integracja z GCP)

reCAPTCHA Enterprise to zarządzana usługa detekcji botów od Google — potomek klasycznej reCAPTCHA, przebudowany jako API na podstawie wyniku, a nie wyzwanie przeszło/nie przeszło. Każde żądanie dostaje wynik od 0.0 (niemal na pewno bot) do 1.0 (niemal na pewno człowiek), plus listę powodów (AUTOMATION, UNEXPECTED_USAGE_PATTERNS, LOW_CONFIDENCE itd.). Co z tym zrobić, decydujesz Ty.

Dwie rzeczy czynią go naturalnym wyborem dla zestawu opartego na GCP:

  1. Niewidoczny domyślnie. W przeciwieństwie do starego interfejsu reCAPTCHA v2 „kliknij wszystkie sygnalizatory”, Enterprise zwykle działa cicho w tle. Użytkownicy nie widzą tarcia, chyba że wynik jest niski, a wtedy możesz podnieść poziom do widocznego wyzwania.
  2. Integruje się z Cloud Armor. Cloud Armor może wyliczyć token reCAPTCHA jako część wyrażenia reguły polityki bezpieczeństwa, więc możesz egzekwować wyniki botów na load balancerze zamiast w aplikacji:
rule {
  action   = "deny(403)"
  priority = 500
  match {
    expr {
      expression = "token.recaptcha_action_token.valid && token.recaptcha_action_token.score < 0.5"
    }
  }
  description = "Block low-score bot traffic to sensitive endpoints"
}

Integracja znaczy, że kod Twojej aplikacji wbudowuje widget JS reCAPTCHA w formularze, wysyła powstały token z każdym żądaniem, a Cloud Armor robi sprawdzenie wyniku, zanim żądanie dotrze do backendu. Sama aplikacja nie musi wiedzieć, czy żądanie jest zaufane.

Jest też tryb właściwego WAF-a w Cloud Armor zwany akcjami zarządzania botami (redirect, googleRecaptcha), który wystawia wyzwanie reCAPTCHA wprost na brzegu — użyteczne dla scenariusza „podejrzany skok ruchu, rzucamy wyzwanie wszystkim, aż opadnie”.

Google zwinął reCAPTCHA Enterprise w szerszą platformę Cloud Fraud Defense, która dodaje klasyfikację agentów AI, agentowy silnik polityk i wyzwania odporne na AI. Istniejące integracje reCAPTCHA Enterprise działają dalej bez zmian — Fraud Defense rozszerza, a nie zastępuje, bez wymaganej migracji i bez zmiany cen. Warto śledzić, jeśli ruch agentów AI stanie się znaczącą częścią tego, co uderza w Twoje formularze.

Tylko klasyfikator (bez integracji po stronie klienta)

Szybszą warstwą — bez widgetu, bez wbudowywania w formularz, bez niczego do zmiany w aplikacji — jest czysty klasyfikator, który wylicza żądania na brzegu, używając sygnałów, które sieć i tak widzi: odcisk TLS (JA3/JA4), kolejność i wielkość liter nagłówków HTTP, czasy żądań, reputacja IP, anomalie wolumetryczne. Modelem jest klasyfikator ML bot/człowiek; żądanie albo przechodzi, albo nie.

  • Cloud Armor Adaptive Protection to natywna opcja GCP. To warstwa ML wewnątrz Cloud Armor, która obserwuje ruch do każdej z Twoich usług backendowych, wykrywa wolumetryczne ataki L7 oraz wzorce upychania poświadczeń / zgarniania niepasujące do żadnej pojedynczej reguły WAF-a i automatycznie generuje kandydacką regułę Cloud Armor, którą możesz wdrożyć (albo dać jej wdrożyć się samej), by zablokować naruszający ruch. Ciągle, bez integracji z aplikacją, bez widocznego dla użytkownika tarcia. Minus: opiera się na wzorcach anomalii ruchu, więc nadużycia wolumetryczne wyłapuje znacznie lepiej niż ataki wolne i ciche od jednego zdeterminowanego aktora.
  • Cloudflare Bot Management to odpowiednik na Cloudflare — klasyfikator ML na każdym żądaniu, wyniki od 1 (bot) do 99 (człowiek), konfigurowalne reguły na wyniku. Siedzi na tej samej warstwie co Cloudflare WAF.
  • Platformy komercyjne do wyrafinowanych nadużyć: DataDome, HUMAN Security (wcześniej PerimeterX), Kasada. Łączą klasyfikatory serwerowe z sondami JS po stronie klienta i są właściwym poziomem, gdy jesteś celowo obrany za cel (spekulacja biletami, przejmowanie kont na skalę, zgarnianie cen przez konkurenta).

Właściwą strukturą dla większości aplikacji jest najpierw klasyfikator, CAPTCHA jako zabezpieczenie: niech pasywny model obsłuży cicho większość ruchu, a do widocznego wyzwania eskaluj tylko te żądania, co do których klasyfikator jest niepewny. reCAPTCHA Enterprise robi to natywnie; połączenie Adaptive Protection (warstwa wolumetryczna) z reCAPTCHA Enterprise (wynik per żądanie, zdolność eskalacji) daje Ci i filtr brzegowy, i awaryjne wyzwanie.

Inne alternatywy

  • hCaptcha — zamiennik reCAPTCHA v2, podobny interfejs, bardziej nastawiony na prywatność, płaci stronom za rozwiązane wyzwania. Używany przez Cloudflare przed zbudowaniem Turnstile.
  • reCAPTCHA v2 / v3 — darmowa, klasyczna wersja (nie Enterprise). Wciąż szeroko używana, ale coraz częściej unikana z powodów dostępności i prywatności, i brakuje jej integracji z Cloud Armor.
  • Arkose Labs — komercyjny, klasy enterprise, wyzwania w stylu gry, bardzo trudne do rozwiązania przez farmy „rozwiązywanie jako usługa”. Drogie. Wybierz to tylko, jeśli Twoja aplikacja jest celem wyrafinowanych, zdeterminowanych operatorów botów.

CAPTCHA to szybki i względnie tani mechanizm odfiltrowania większości oportunistycznego ruchu botów — większość zautomatyzowanych nadużyć jest nieukierunkowana, poddaje się w chwili, gdy trafi na wyzwanie, a każda produkcyjna aplikacja z formularzem rejestracji bez niego dostaje nietrywialną część rejestracji od botów. Nie są jednak pełnym rozwiązaniem: zdeterminowany atakujący może płacić usługom rozwiązywania CAPTCHA około $1–3 za 1000 wyzwań, co wystarcza, by odstraszyć nadużycia oportunistyczne, ale nie ukierunkowaną kampanię, a widoczne wyzwania dodają tarcie, szkodzą konwersji i są naprawdę wrogie części użytkowników pod kątem dostępności.

Więc są konieczne, lecz niewystarczające. Praktyczne podejście:

  • Zawęź je do endpointów o wysokiej wartości i podatnych na nadużycia — rejestracja, logowanie, reset hasła, formularze kontaktowe, realizacja kuponów, wszędzie, gdzie aplikacja wysyła maila albo tworzy zasób. Nie na stronach tylko do czytania.
  • Integracja na podstawie wyniku (reCAPTCHA Enterprise, Turnstile) jest nieporównywalnie lepsza od klasycznych wyzwań przeszło/nie przeszło pod kątem UX — większość prawdziwych użytkowników nigdy nie widzi wyzwania.
  • Łącz z ograniczaniem tempa, nie zamiast niego. CAPTCHA na formularzu logowania i agresywne ograniczanie tempa per IP razem są znacznie mocniejsze niż każde osobno.
  • Nie stawiaj CAPTCHY na wewnętrznych stronach administracyjnych. Schowaj je za uwierzytelnieniem i listami dozwolonych IP — ochrona przed botami jest dla formularzy, które muszą pozostać publiczne.

Za brzegiem — skanowanie zależności z OSV-Scanner

Poprzednie warstwy albo hartują brzeg (WAF, ochrona przed botami), albo porządkują repozytorium (sekrety). Żadna z nich nie dotyka bibliotek ze znanymi lukami, które już działają wewnątrz Twojej aplikacji — a to właśnie tam faktycznie zaczyna się większość eksploatacji legacy. To naprawia skanowanie zależności.

Dlaczego OSV-Scanner

OSV-Scanner to otwartoźródłowe CLI od Google, zbudowane na bazie bazy danych OSV (Open Source Vulnerabilities). OSV to źródło danych o lukach, które pod spodem odpytują Dependabot, GitHub Security Advisories, GCP Container Analysis i kilka innych skanerów. Jest to znormalizowany, czytelny maszynowo schemat agregujący ostrzeżenia z npm, PyPI, Go, Cargo, RubyGems, Maven, Packagist, NuGet, pakietów Debiana/Alpine/Ubuntu, GitHub Actions i innych.

Czyta popularne pliki lock (package-lock.json, yarn.lock, Pipfile.lock itd.), rozwiązuje każdy pakiet w jego dokładnie przypiętej wersji i raportuje luki z identyfikatorami CVE, wagą i (gdzie OSV to ma) zakresem wersji, który je naprawia.

Uruchamiamy skan i czytamy wyjście

OSV-Scanner da się zainstalować przez Go, Homebrew, gotowym binarnym plikiem albo — tak samo jak gitleaks powyżej — uruchomić przez Dockera. Dla spójności pokażemy Dockera.

Właśnie uruchomiłem to na repozytorium, w którym żyje ten artykuł:

docker run --rm -v "$(pwd):/src" ghcr.io/google/osv-scanner:latest \
    scan source --recursive /src

--recursive przechodzi przez każdy katalog pod /src i znajduje pliki lock w zagnieżdżonych projektach (blog, kilka dem, serwer, eksperymenty szkicowe). Bez tego skaner inspekcjonuje tylko górny poziom — w porządku dla repozytorium jednoprojektowego, ale większość prawdziwych ma pliki lock rozsiane po kilku katalogach.

Podsumowanie na końcu przebiegu wygląda tak:

Total 35 packages affected by 59 known vulnerabilities (2 Critical, 17 High, 37 Medium, 3 Low, 0 Unknown) from 2 ecosystems.
59 vulnerabilities can be fixed.

Pełne wyjście to tabela z jednym wierszem na CVE × pakiet × źródło prawdy. Kilka reprezentatywnych wierszy:

OSV URLCVSSEkosystemPakietWersjaWersja naprawionaŹródło
GHSA-xq3m-2v4x-88gg9.4npmprotobufjs7.5.47.5.5blog/…/transformers-js-demo/package-lock.json
GHSA-p9ff-h696-f5838.2npmvite7.3.17.3.2blog/package-lock.json
PYSEC-2025-407.5PyPItransformers4.48.34.49.0blog/…/from-scratch/requirements.txt
GHSA-r5fr-rjxr-66jc8.1npmlodash4.17.234.18.0server/package-lock.json

Sześć pól na wiersz, wszystkie użyteczne:

  • OSV URL — prowadzi prosto do ostrzeżenia na osv.dev (identyfikator CVE, wektor ataku, zakres dotkniętych wersji, odnośniki).
  • CVSS — wynik wagi na skali 0–10. 9+ to Critical, 7–8.9 to High, 4–6.9 to Medium.
  • Ecosystem — z którego rejestru pakietów pochodzi zależność. Ta sama nazwa pakietu może istnieć w wielu ekosystemach i dostawać różne CVE.
  • Package i Version — co jest obecnie przypięte w pliku lock.
  • Fixed version — najniższa wersja, która rozwiązuje problem. Często podniesienie łatki; czasem minor albo major.
  • Source — z którego pliku lock przyszło znalezisko. Krytyczne dla monorepozytoriów: ten sam pakiet może być przypięty w różnych wersjach w różnych podprojektach, co dokładnie widzimy dla vite, protobufjs i picomatch w rzeczywistym skanie.

Zauważ, że podsumowanie rozbija licznik na dwa sposoby: 59 znanych luk ogółem i 59 luk da się naprawić — czyli każda ma znany cel podniesienia. W tym skanie się pokrywają, co nie zawsze jest regułą.

Kolumna Fixed version mówi Ci, czy dla każdego wiersza istnieje droga podniesienia. Jeśli jest wypełniona, skaner już zrobił rozpoznanie; pozostała praca to wyłącznie podniesienie — podbij, przetestuj, wyślij. Jeśli jest pusta, trafiłeś na znalezisko bez znanej jeszcze naprawy i reakcja jest inna. Następna sekcja przechodzi przez to, jak triażować oba rodzaje.

Skaner może też wziąć na cel konkretny plik lock (--lockfile=/path/to/lockfile), SBOM w formacie SPDX albo CycloneDX (--sbom=/path/to/sbom.json) albo obraz kontenera (osv-scanner scan image my-app:latest) — skanowanie obrazów potrzebuje lokalnej instalacji CLI, a nie Dockera, bo skaner potrzebuje bezpośredniego dostępu do systemu plików inspekcjonowanego obrazu.

Co zrobić ze znaleziskami

Uruchom to raz na aplikacji odziedziczonej i niemal na pewno dostaniesz ścianę znalezisk — łatwo dziesiątki, czasem setki. Dla każdego znaleziska o tym, co zrobić, decydują dwa pytania: jak poważne to jest i czy jest dostępna naprawa.

Znaleziska naprawialne. Większość tego, co wyrzuca skan, zwykle wpada tutaj, bo aktywnie utrzymywane pakiety dostają łatki. Triażuj po wadze:

  1. Critical/High ze znanym publicznym eksploitem — łataj w pierwszej kolejności, niezależnie od tego, czy sądzisz, że używasz tej ścieżki kodu. Publicznej eksploatacji nie interesuje Twój model mentalny kodu.
  2. Critical/High bez znanego eksploitu — łataj w normalnym rytmie (następny sprint).
  3. Medium/Low — podbijaj w następnym rutynowym cyklu podnoszenia. Nie pozwól im blokować builda, bo nigdy nie zobaczysz zielonego przebiegu CI na aplikacji odziedziczonej.

Dla wszystkich tych praca ma ten sam kształt: podbij przypiętą wersję do Fixed version, uruchom testy, wyślij. Narzędzia jak Dependabot i Renovate (omówione niżej) automatyzują krok tworzenia PR-a.

Znaleziska nienaprawialne. Potrzebują innej obsługi i zwykle są trzy odmiany:

  1. Łatka jeszcze nie wydana — luka została ujawniona, ale utrzymujący nie wysłał naprawy. Śledź to, skanuj okresowo ponownie i pilnuj strony ostrzeżenia OSV pod kątem aktualizacji.
  2. Nieutrzymywany pakiet — projekt jest martwy. Albo zamień zależność, albo żyj z nią świadomie i dodaj udokumentowane stłumienie w osv-scanner.toml, żeby skan nie flagował jej ponownie przy każdym przebiegu CI.
  3. Zależność przechodnia, której nie możesz podnieść bezpośrednio — podatny pakiet jest wciągany przez coś innego, co nie podbiło się do nowszej wersji rodzica. To dokładnie ten przypadek, w którym wyjście OSV-Scannera łączy się z WAF-em: jeśli nie ma drogi podniesienia, a luka ma znany wzorzec eksploatacji, napisz regułę WAF blokującą ten wzorzec jako łatę wirtualną, dopóki nie wyląduje naprawa z góry.

Podział ma znaczenie operacyjnie: znaleziska naprawialne to praca dla Twojego potoku budowania (PR-y Dependabota, zautomatyzowane podniesienia). Znaleziska nienaprawialne to praca dla człowieka — decyzja o stłumieniu, zamianie albo wirtualnym załataniu — i to na nie warto uważać, bo same z siebie nie znikają.

Alternatywy i kiedy którą wybrać

  • GCP Artifact Analysis — natywna opcja GCP i prawdopodobnie skaner o najmniejszym tarciu, jaki możesz dodać, jeśli już jesteś na GCP. Artifact Registry auto-skanuje obrazy przy pushu, używając bazy OSV (to samo źródło danych co OSV-Scanner powyżej), a ciągła analiza skanuje przechowywane obrazy ponownie w miarę pojawiania się nowych luk w OSV — więc dowiadujesz się o świeżo ujawnionych CVE w obrazach wysłanych miesiące temu, bez ponownego pushu. Znaleziska pokazują się w interfejsie Artifact Registry i jako powiadomienia Pub/Sub. Żadnego dodatkowego narzędzia do prowadzenia.
  • Trivy — prawdziwy nadzbiór, jeśli chcesz jedno narzędzie. Skanuje kontenery, systemy plików, repozytoria gita, konfiguracje Kubernetesa, IaC (Terraform/CloudFormation) i sekrety. Wybierz Trivy, jeśli potrzebujesz też skanowania kontenerów i IaC i chcesz jednego narzędzia operacyjnego działającego wszędzie.
  • Dependabot — jeśli jesteś na GitHubie, jest już podłączony. Otwiera PR-y podnoszące podatne zależności automatycznie. Używaj go obok OSV-Scannera: Dependabot do auto-PR-ów, OSV-Scanner w CI do blokowania builda po wadze.

Rozsądny minimalny zestaw: OSV-Scanner w CI (blokuje build na nowych znaleziskach wysokiej wagi) + Dependabot (ciągle otwiera PR-y podnoszące). Jeśli już wysyłasz kontenery, albo dodaj Trivy do skanowania obrazów, albo — jeśli pushujesz do rejestru chmurowego — opieraj się na wbudowanym skanerze rejestru (Artifact Analysis na GCP, ECR + Inspector na AWS), a nie prowadź kolejnego narzędzia.

Skanery to nie higiena łańcucha dostaw

Jedna ważna luka warta nazwania: skanery wyłapują znane CVE w przypiętych zależnościach. Nie wyłapują ataków na łańcuch dostaw — złośliwych pakietów, skompromitowanych kont utrzymujących, nazw-literówek (lodsh vs lodash), skryptów post-install wyprowadzających poświadczenia. To inna klasa zagrożeń i wymaga innych obron: wymuszania plików lock, wyłączania skryptów cyklu życia przy instalacji, przeglądania nowych zależności przed dodaniem, 2FA na kontach rejestrów pakietów.

NPM Security Cheat Sheet od OWASP to użyteczna konkretna lista kontrolna do tego, jeśli Twój stos to Node.js — obejmuje --ignore-scripts, npm audit, pakiety w scope, wypatrywanie literówek i więcej. Vulnerable Dependency Management Cheat Sheet od OWASP pokrywa ten sam grunt niezależnie od języka. Oba dobrze łączą się z warstwą opartą na skanerach powyżej: skanery na znane-złe, higiena na nieznane-złe.

Co ten zestaw pokrywa i gdzie można pójść dalej

Jeśli wdrożysz trzy podstawowe warstwy plus skanowanie zależności, co właściwie sobie kupiłeś?

Pokryte całkiem porządnie:

  • Oportunistyczne automatyczne skanery i kampanie masowej eksploatacji.
  • Znane CVE w zależnościach — o ile łatasz je, gdy znaleziska przychodzą.
  • Wyciekłe poświadczenia w repozytorium (bieżące i historyczne).
  • Wstrzyknięcie SQL, XSS, przechodzenie po ścieżkach i wstrzyknięcie komend w czasie żądania — blokowane przez WAF-a, zanim dotrą do aplikacji.
  • Oczywista, wolumenowa warstwa nadużyć botów na publicznych formularzach.

Wciąż pominięte:

  • Podatne wzorce wewnątrz Twojego własnego kodu. WAF blokuje ładunki w czasie żądania, ale nie pomaga Ci znaleźć ani naprawić leżącego pod tym podatnego kodu. SAST i DAST poniżej to sposób, by zacząć to robić.
  • Wady logiki aplikacji — wszystko, co wymaga wiedzy, co aplikacja ma robić. IDOR (żądanie do /orders/1234, gdy użytkownik powinien widzieć tylko 1233), zepsute uwierzytelnianie albo zarządzanie sesją, brakujące lub niespójne sprawdzenia autoryzacji, błędy logiki biznesowej w rodzaju pominięcia kroku płatności przez powtórne wysłanie żądania finalizacji koszyka. Żadne z tych nie wygląda złośliwie dla skanera.
  • Nowe dni zerowe w aplikacji albo jej zależnościach, przed istnieniem sygnatur.
  • Zdeterminowani przeciwnicy. Insiderzy nadużywający legalnego dostępu. Dobrze uposażeni operatorzy botów, którzy płacą usługom rozwiązywania CAPTCHA i rotują IP szybciej, niż zdążysz ich blokować.

Co dodać w następnej kolejności: SAST i DAST

Gdy trzy podstawowe warstwy i skanowanie zależności są na miejscu, naturalnym następnym krokiem jest analizowanie samego kodu, a nie tylko ruchu przezeń płynącego. Dwie komplementarne techniki:

  • SAST (Static Application Security Testing) czyta źródła statycznie i dopasowuje wzorce ryzykownych konstrukcji — zapytania SQL budowane sklejaniem łańcuchów, brakujące sprawdzenia autoryzacji, komendy powłoki składane z wejścia użytkownika, niewalidowane przekierowania, poświadczenia na twardo. Semgrep to otwartoźródłowy domyślny wybór; utrzymywane społecznościowo zestawy reguł jak p/security-audit i p/owasp-top-ten dają pokrycie w większości języków bez zachodu z konfiguracją CodeQL albo SonarQube. Uruchamiaj w CI i blokuj build na znaleziskach wysokiej wagi.
  • DAST (Dynamic Application Security Testing) robi odwrotnie — uruchamia aplikację i sonduje ją z zewnątrz, przeczesując trasy i próbując prawdziwych ładunków atakujących. Potężne dla aplikacji-czarnej-skrzynki właśnie dlatego, że nie potrzebuje kodu źródłowego; skaner jest syntetycznym atakującym. OWASP ZAP to otwartoźródłowy domyślny wybór, z pasywnym skanem bazowym, bezpiecznym względem stagingu albo produkcji, i destrukcyjnym pełnym aktywnym skanem, który powinien uruchamiać się wyłącznie względem stagingu z danymi jednorazowymi. Burp Suite to komercyjny standard branżowy; Nuclei dopełnia którykolwiek szybkim pokryciem wzorców CVE opartym na szablonach.

Rozsądne rozszerzenie zestawu: Semgrep na każdym PR-ze, skan bazowy ZAP na każdym wdrożeniu na staging, pełny uwierzytelniony skan ZAP według harmonogramu nocnego albo tygodniowego. Uwierzytelnione skanowanie ma nieproporcjonalne znaczenie dla DAST na aplikacji odziedziczonej — nieuwierzytelniony skan widzi tylko stronę logowania; wszystkie interesujące endpointy są za nią.