ModSecurity w praktyce: od 403 do wąskiego wyjątku OWASP CRS
Od zgłoszenia 403 do wąskiego wyjątku OWASP CRS: audyt transakcji, właściwa reguła, zakres zmiany i walidacja na wspólnej warstwie Nginx.

Legalny formularz kończy się 403, a w logu ostatnia widoczna reguła to 949110. Najłatwiej usunąć blokadę. Trudniej ustalić, co ją wywołało, i przepuścić wyłącznie uzasadniony przypadek. Ten drugi wariant był podstawą pracy przy wspólnej warstwie Nginx chroniącej kilka środowisk nieprodukcyjnych.
ModSecurity działał tam z SecRuleEngine On. Diagnostyka opierała się na audycie JSON, a wyjątki trafiały do lokalnego pliku, poza regułami dostarczanymi przez OWASP CRS. Istotą rozwiązania nie było znalezienie uniwersalnej listy reguł do wyłączenia. Było nią ograniczenie każdej zmiany do właściwego żądania i wykazanie, dlaczego właśnie ten fragment wymaga wyjątku.
ModSecurity i OWASP CRS — kto tu właściwie blokuje ruch?
WAF, czyli web application firewall, to warstwa ochrony aplikacji webowej, która analizuje ruch HTTP. Interesuje ją treść wymiany z aplikacją: żądanie, jego elementy oraz odpowiedź. Dzięki temu decyzja o przepuszczeniu ruchu może wynikać z tego, co klient przesyła, a nie tylko z faktu, że nawiązał połączenie z serwerem. Dla administratora oznacza to dodatkowe miejsce kontroli przed obsługą żądania przez aplikację.
ModSecurity jest silnikiem takiej inspekcji. Potrafi analizować i filtrować żądania oraz odpowiedzi HTTP, a jednym z serwerów, z którymi współpracuje, jest Nginx. W układzie reverse proxy pozwala więc umieścić kontrolę na wejściu do chronionych aplikacji. Samo słowo „ModSecurity” nie opisuje jednak jeszcze, jakie wzorce ataków będą wykrywane — silnik potrzebuje reguł określających, czego ma szukać i jak reagować. Tak rozdziela te role projekt OWASP ModSecurity.
OWASP Core Rule Set, w skrócie CRS, dostarcza zestaw reguł detekcji. Obejmują one między innymi SQL injection, cross-site scripting, czyli XSS, oraz ataki związane z dostępem do plików, takie jak path traversal i local file inclusion. Są to reguły ogólnego zastosowania, przygotowane do ochrony różnych aplikacji. Najkrótsze rozróżnienie brzmi: ModSecurity to silnik, CRS to reguły, które ten silnik wykonuje. Opis zakresu ochrony udostępnia projekt OWASP CRS.
Taki układ daje wspólną kontrolę ruchu kierowanego do aplikacji, ale reguły ogólne nie znają wszystkich jej formularzy i dozwolonych sposobów użycia. Legalna wartość pola może przypominać wzorzec ataku. Gdy zostanie błędnie rozpoznana, mówimy o false positive. Dlatego skuteczność konfiguracji trzeba oceniać również na rzeczywistych operacjach użytkowników, a nie jedynie po poprawnym uruchomieniu silnika.
Wyłączenie całego WAF usuwałoby także kontrolę niezwiązaną z takim formularzem. Strojenie powinno dotyczyć konkretnego nietrafnego dopasowania i zachowywać resztę ochrony — to podejście zaleca dokumentacja false positives CRS. Właśnie od rozpoznania takiego dopasowania zaczyna się opisany przypadek 403.
Najpierw transakcja, potem konfiguracja
W tym przypadku zgłoszenie 403 rozpoczynało analizę konkretnej operacji użytkownika. Zbierano czas, host, metodę HTTP, URI oraz informację, co użytkownik próbował zrobić. Dopiero z takim zestawem szukano transakcji w /var/log/modsec_audit.log. Wspólna warstwa wejściowa wymagała uwzględnienia hosta — sam adres ścieżki nie opisywał całego zakresu wyjątku.
Praktyczna rekomendacja: poproś o ponowienie jednej operacji i zanotuj jej czas. Nie mieszaj kilku prób w jedną historię. Przy analizie dopasuj czas do zapisu w logu, a host, metodę i URI porównaj z rzeczywistym żądaniem. Jeśli dostajesz kilka kandydatów, nie wybieraj wygodnego wpisu tylko dlatego, że zawiera znane ID reguły.
Po odnalezieniu wpisu identyfikator unique_id, jeśli jest obecny, pozwala wrócić do tej samej transakcji. To dobry punkt odniesienia dla notatki diagnostycznej: wskazuje analizowany przypadek, zamiast odsyłać do całego pliku. W zachowanej dokumentacji tego wdrożenia nie ma kompletnego, zanonimizowanego łańcucha „transakcja → finalny wyjątek → wynik testu”. Dlatego nie odtwarzamy przykładowego logu ani nie przypisujemy mu wymyślonego wyniku.
Poniższe polecenie jest syntetycznym narzędziem pomocniczym, nie zapisem komendy z incydentu. Zakłada układ JSON z obiektem transaction. Najpierw potwierdź strukturę swojego logu; w niektórych wdrożeniach pola hosta lub organizacja rekordów będą inne. Uzupełnij trzy wartości rzeczywistymi danymi wyłącznie w swoim środowisku:
HOST='panel.example.test'
METHOD='POST'
URI='/editor/save'
sudo jq -r --arg host "$HOST" --arg method "$METHOD" --arg uri "$URI" '
.transaction
| select(.request.method == $method and .request.uri == $uri)
| select((.request.headers.Host // .request.hostname // "") == $host)
| [.time_stamp, .unique_id, .request.method, .request.uri,
(.response.http_code | tostring)]
| @tsv
' /var/log/modsec_audit.log
Wynik służy do dalszego dopasowania czasu zgłoszenia. Nie zawężamy go do 403, dzięki czemu podobny odczyt przyda się również po zmianie. Puste wyjście nie dowodzi, że WAF nie uczestniczył w blokadzie: sprawdź strukturę danych oraz dokładną postać hosta i URI, w tym port lub query string. Nie usuwaj tych różnic przez coraz szerszy filtr bez zrozumienia żądania. Składnię przetwarzania opisuje podręcznik jq.
Co czytać w messages, a czego nie zgadywać
W rzeczywistej diagnostyce analizowano ruleId, data, match oraz nazwę zmiennej, zwłaszcza ARGS:<parametr>. Te elementy odpowiadają na różne pytania. Sam numer reguły nie wystarcza do zaprojektowania wyjątku.
| Element | Pytanie administratora |
|---|---|
messages[].details.ruleId |
Która reguła zgłosiła dopasowanie? |
data |
Jakie dane zostały wskazane przy dopasowaniu? |
match |
Co dokładnie porównano i z jaką zmienną? |
ARGS:<parametr> |
Które pole żądania wymaga analizy? |
Po wyborze transakcji można odczytać jej komunikaty. To również przykład syntetyczny, dla układu pól stosowanego przez libModSecurity; nie zawiera fikcyjnego wyniku. Zastąp znacznik prawdziwym identyfikatorem z własnego logu:
ID='WSTAW_UNIQUE_ID_Z_WLASNEGO_LOGU'
sudo jq --arg id "$ID" '
select(.transaction.unique_id == $id)
| .transaction.messages[]?
| {ruleId: .details.ruleId,
message: .message,
data: .details.data,
match: .details.match}
' /var/log/modsec_audit.log
Nazwy pól tego wzorca można porównać z implementacją JSON w ModSecurity. Nie zakładaj, że każda integracja zapisuje identyczny zestaw informacji. Brak pola jest powodem do sprawdzenia formatu, a nie dopisania wartości z pamięci.
W kontekście projektu zachowały się ID 941320, 942131 i 942430. Nie mamy jednak źródła wiążącego każde z nich z konkretnym endpointem i argumentem. Są więc kontekstem diagnostycznym, nie listą wyjątków do skopiowania. Nie ma podstaw, by na ich podstawie wyłączyć trzy reguły dla dowolnego pola formularza.
Dlaczego nie wyłączaliśmy 949110
W opisywanym wdrożeniu świadomie oddzielano regułę wykrywającą problem od końcowej reguły blokującej. 949110 traktowano jako podsumowanie i egzekwowanie wyniku anomaly score, nie jako cel naprawy false positive.
CRS rozdziela detekcję od decyzji o blokadzie: dopasowania zwiększają wynik, a etap oceny porównuje go z progiem. Wyłączenie końcowej kontroli nie wyjaśnia, dlaczego legalne pole zostało uznane za podejrzane. Zmienia decyzję ochrony zamiast zawęzić nietrafne dopasowanie. Mechanikę opisuje dokumentacja anomaly scoring.
Wniosek operacyjny był prosty: wrócić do komunikatów tej samej transakcji, znaleźć właściwe ID oraz argument i dopiero wtedy ocenić zasadność wyjątku. Nie „naprawiać” objawu przez wyłączanie 949110, całego CRS lub ochrony aplikacji.

False positive trzeba uzasadnić
Przed zmianą sprawdzano, czy żądanie odpowiada oczekiwanemu działaniu aplikacji, czy parametr jest właściwy dla endpointu i czy dopasowanie da się odtworzyć. To ważniejsza część pracy niż samo zapisanie dyrektywy. Komunikat „formularz jest nasz” nie odpowiada jeszcze na pytanie, dlaczego właśnie to pole powinno ominąć właśnie tę regułę.
Dobrym zapisem decyzji jest krótka notatka zawierająca pięć wymiarów: host, URI, metoda, parametr i ID reguły. Do tego powód uznania dopasowania za nietrafne oraz sposób ponowienia testu. Jest to rekomendowana forma dokumentowania decyzji, nie cytat z konkretnego zgłoszenia.
Jeśli nie potrafisz wskazać któregoś wymiaru, zatrzymaj zmianę i wróć do transakcji. Nie zastępuj brakującej wiedzy wyłączeniem kategorii reguł. Wspólny reverse proxy wymusza szczególną ostrożność: wyjątek powinien opisywać konkretną operację, nie ogólną frustrację po kolejnym 403.
Jak zapisać wąski wyjątek
Poniższy fragment to przykład syntetyczny oparty na publicznej składni ModSecurity, nie reguła odzyskana z wdrożenia. Host, endpoint i parametr są wymyślone na potrzeby objaśnienia. ID 941320 występuje tu jako przykład; nie przypisujemy mu takiego dopasowania w naszym case.
Założenie wzorca: analiza własnej transakcji uzasadniła pominięcie jednego parametru przez jedną regułę dla dokładnie wskazanej operacji. Zamiast kompletnej konfiguracji pokazujemy samą akcję. PRZYKŁAD SYNTETYCZNY — składnia akcji zweryfikowana z manualem ModSecurity v3, ale nie przetestowana integracyjnie w opisywanym środowisku. Nie jest to samodzielna dyrektywa ani gotowy plik do wklejenia:
UWAGA: poniższy fragment NIE jest samodzielną implementacją wyjątku. Użyty w regule bez warunków host/URI/metoda może utworzyć szerszy wyjątek niż zamierzony. Nie wklejaj go jako gotowego rozwiązania.
# PRZYKLAD SYNTETYCZNY — fragment listy akcji SecRule.
ctl:ruleRemoveTargetById=941320;ARGS:content
Manual w sekcji ctl pokazuje składnię ruleRemoveTargetById z ID i argumentem ARGS:. Tutaj docelowym wyłączeniem jest para reguła–parametr, nie wszystkie argumenty żądania. Warunki zawężające przedstawiamy osobno, jako pseudokod, nie składnię ModSecurity:
JEŚLI host == "panel.example.test"
ORAZ URI == "/editor/save"
ORAZ metoda == "POST"
TO dla tej transakcji pomiń ARGS:content w regule 941320
Odpowiadające tym warunkom zmienne opisują sekcje REQUEST_HEADERS, REQUEST_METHOD i REQUEST_URI. Przy implementacji trzeba sprawdzić sposób wykonania akcji w konkretnym układzie reguł.
Według sekcji chain łańcuch symuluje logiczne AND. Akcje disruptive i sterujące przepływem ze startera wykonują się po dopasowaniu całego łańcucha. Akcje non-disruptive, do których należy ctl, mogą znajdować się w jego dowolnym elemencie i wykonują się, gdy dopasuje reguła zawierająca akcję. Nie zakładaj zatem, że samo dodanie chain odracza każdą akcję do końca wszystkich sprawdzeń.
Wzorzec używa dokładnych porównań. Nie obejmuje automatycznie hosta z dopisanym portem, innej pisowni hosta, innego URI ani wariantu z query stringiem. Najpierw sprawdź wartości widziane przez silnik. Nie rozszerzaj dopasowania do całego hosta tylko po to, żeby przykład zaczął działać.
Wyjątek runtime powinien zostać wczytany przed regułami CRS, na które wpływa. Sprawdź też strukturę reguły docelowej: wyłączenia targetów mają ograniczenia dla reguł łańcuchowych. Nie zamieniaj tego ograniczenia w automatyczną zgodę na wyłączenie całej reguły. Te warunki opisuje dokumentacja strojenia CRS.
Lokalny plik musi naprawdę uczestniczyć w konfiguracji
W przypadku opisanym w źródłach wyjątki utrzymywano w /etc/nginx/modsecurity/local-exclusions.conf. Nie edytowano bezpośrednio plików OWASP CRS. Taka separacja była częścią polityki wdrożenia, a nie jednorazowym obejściem.
Dokumentacja operacyjna zawierała również ostrzeżenie przed kopiowaniem konfiguracji Nginx 1:1 bez przejrzenia aktywnych include'ów i wyjątków. Plik leżący na dysku nie dowodzi, że obsługuje żądanie. Zalecaną kontrolą był odczyt przez nginx -T oraz sprawdzenie ścieżek konfiguracji.
sudo nginx -T
nginx -T wykonuje test konfiguracji jak -t i dodatkowo wypisuje odczytaną konfigurację. W tej procedurze nginx -t jest jawnym krokiem walidacji przed reloadem, a nginx -T pomaga sprawdzić używane pliki i include’y. Nie jest odczytem konfiguracji z pamięci działających workerów. Uruchamiaj go dla właściwego binarnego Nginx i tego samego pliku głównego, którego używa usługa. Następnie prześledź plik reguł ModSecurity i jego include'y; nie zakładaj, że nginx -T rozwinie cały niezależny zestaw reguł silnika. Znaczenie opcji -T i -t wyjaśnia dokumentacja Nginx.
Walidacja kończy się na żądaniu, nie na nginx -t
Przed edycją zachowaj wersjonowaną kopię zmienianego pliku i informacje potrzebne do odtworzenia jego uprawnień oraz wymaganych metadanych. To zalecenie dla powtarzalnej procedury, nie twierdzenie o zachowanym backupie z incydentu. Przygotuj też sposób wycofania wyłącznie swojej zmiany, jeśli w międzyczasie konfigurację edytuje ktoś inny.
W case stosowano test konfiguracji przed reloadem, a po zmianie ponawiano żądanie i wracano do audytu. Ogólny wariant poleceń, dla właściwej instancji Nginx i jej domyślnej ścieżki konfiguracji, wygląda tak:
sudo nginx -t
# Dopiero po sukcesie testu wykonaj kolejny krok:
sudo systemctl reload nginx
Wykonuj te kroki osobno: po błędzie testu zatrzymaj procedurę i nie uruchamiaj reloadu. Przy wielu instancjach lub niestandardowej konfiguracji jawnie wybierz właściwy binary, plik konfiguracji i usługę; pokazany przykład zakłada usługę nginx zarządzaną przez systemd. Sam sukces polecenia nie zastępuje sprawdzenia przebiegu przeładowania w logach usługi. Nginx przy reloadzie próbuje zastosować nową konfigurację; jeśli nie może jej zastosować, zachowuje starą. Szczegóły opisuje procedura sterowania Nginx.
Powtórz dokładnie operację, która była blokowana. Porównaj host, metodę, URI i pole, nie tylko kod odpowiedzi. Ponownie obejrzyj audit log. Dodatkowo zaplanuj kontrolę żądań poza zakresem wyjątku: innego endpointu, metody lub parametru. To rekomendacja weryfikacji granic, nie deklaracja, że w tym case wykonano określony zestaw testów negatywnych.
Rollback powinien odwracać lokalną zmianę, nie wyłączać ModSecurity. Cofnij dodany wyjątek albo odtwórz sprawdzoną wersję pliku, jeśli nie utracisz nowszych edycji. Sprawdź wymagane metadane, wykonaj ponownie test konfiguracji i dopiero po jego sukcesie reload. Następnie powtórz kontrolę żądania oraz audytu. Cofnięcie wyjątku może przywrócić wcześniejszy 403 — uwzględnij to w decyzji o wycofaniu.
Przed zamknięciem zgłoszenia
- Mam właściwą transakcję: czas, host, metoda i URI zgadzają się ze zgłoszeniem.
- Wiem, co wskazują
ruleId,data,matchi konkretneARGS:*. - Potrafię uzasadnić false positive, zamiast wyłączać znany numer na próbę.
- Wyjątek obejmuje tylko potrzebny host, endpoint, metodę, parametr i regułę.
949110oraz upstream CRS nie zostały wyłączone ani zmodyfikowane jako skrót do usunięcia 403.- Sprawdziłem include'y, wynik
nginx -ti przeładowanie właściwej instancji. - Powtórzyłem operację, wróciłem do audytu i mam przygotowane wycofanie zmiany.
Z tego wdrożenia warto zapamiętać kolejność: transakcja, dowód dopasowania, decyzja o wyjątku, a dopiero potem konfiguracja. Zniknięcie 403 jest początkiem walidacji. Jej celem jest uzasadniona, ograniczona zmiana ochrony.