Under The Hood 9.8.3
English version: Under The Hood 9.8.3
To wydanie to cykl konserwacji i wzmacniania zabezpieczeń, a nie wprowadzenie funkcji. Większość prac została wykonana tam, gdzie właściciele witryn rzadko zaglądają: na warstwie danych, która odczytuje logi bezpieczeństwa, kodzie, który tworzy powiadomienia e-mail, oraz ścieżkach eksportu, które przesyłają plik CSV z rekordami aktywności. Żadna z tych czynności nie zmienia wyglądu wtyczki, ale wszystkie wpływają na jej niezawodność i bezpieczeństwo, gdy witryna jest obciążona, gdy logi są duże lub gdy atakujący bada przypadek brzegowy, którego jeszcze nie zamknęliśmy.
Poniżej znajduje się opis wdrożenia, przeznaczony dla administratorów i deweloperów, którzy chcą zrozumieć, co faktycznie się zmieniło.
Zamykanie wektora wstrzykiwania adresatów wiadomości e-mail
WP Cerber wysyła dwa rodzaje e-maili transakcyjnych z osadzonym imieniem i nazwiskiem użytkownika: wiadomość PIN z uwierzytelnianiem dwuskładnikowym oraz powiadomienie o alertach aktywności. Oba typy tworzą odbiorcę w znanym formacie RFC 5322, czyli Name <email> , i oba pobierają tę nazwę bezpośrednio z pól profilu WordPress ( user_firstname , user_lastname i display_name ) bez użycia znaku ucieczki.
Problem tkwi w sposobie, w jaki wp_mail() obsługuje adresata w postaci ciągu znaków. Funkcja ta dzieli ciąg znaków na podstawie przecinka i nie uwzględnia granic między ciągami w cudzysłowie. Użytkownik, który umieścił odpowiednie znaki w nazwie swojego profilu, mógłby zatem wstrzyknąć dodatkowy adres do listy odbiorców wiadomości e-mail z kodem PIN uwierzytelniania dwuskładnikowego wygenerowanej przez funkcję CRB_2FA::send_user_pin() oraz wiadomości e-mail z alertem wygenerowanej przez gałąź user_list funkcji cerber_get_email() . W witrynie, w której każdy może się zarejestrować i ustawić nazwę wyświetlaną, jest to realna ścieżka do dyskretnego przekierowania kopii wiadomości bezpieczeństwa.
Dodaliśmy dedykowany sanitizer, crb_sanitize_mail_display_name() , w cerber-common.php i zastosowaliśmy go w obu miejscach wywołań. Usuwa on dokładnie te znaki, które są istotne dla tej klasy iniekcji: przecinek, cudzysłów, ukośnik odwrotny, nawiasy kątowe i znaki kontrolne. Widoczna nazwa nadal wyświetla się poprawnie dla uprawnionych użytkowników, a lista odbiorców nie może być już sterowana przez zmodyfikowane pole profilu.
Usuwanie zapisanego wektora XSS w skanerze własności
Skaner złośliwego oprogramowania zgłasza zmianę właściciela zainstalowanej wtyczki w repozytorium WordPress.org. W tym celu pobiera metadane dotyczące właściciela dostarczane przez to repozytorium i do tej pory wplatał część tych metadanych w powiadomienie administratora w postaci surowego kodu HTML, w tym ręcznie tworzone znaczniki kotwic dla linków do profilu właściciela.
Repozytorium jest zaufanym źródłem, a prawdopodobieństwo wrogich metadanych jest niskie, ale dane zewnętrzne nigdy nie powinny docierać do interfejsu administratora w postaci nieskorygowanego znacznika. Traktowanie go jako zaufanego to właśnie założenie, na którym wolimy nie polegać.
Aby poprawnie zamknąć wektor, zamiast łatać go lokalnie, wprowadziliśmy nowy element fabryki interfejsu użytkownika (UI Factory), formatted_text , skonstruowany za pomocą funkcji pomocniczej crb_ui_formatted_text() . Renderuje on szablon zwykłego tekstu, który zawiera ponumerowane symbole zastępcze %N$s . Dosłowne fragmenty szablonu i argumenty skalarne są zabezpieczane znakami ucieczki HTML, podczas gdy każdy argument, który sam jest elementem interfejsu użytkownika, jest renderowany przez aktywny renderer. Każda wartość dynamiczna jest zatem zabezpieczana znakami ucieczki w kontekście, w którym jest faktycznie emitowana, co jest jedynym niezawodnym sposobem zapobiegania błędom kontekstu wyjściowego.
Komunikat o właścicielu w crb_check_ownership() korzysta teraz z tego elementu, a linki do profili właścicieli są budowane za pomocą crb_ui_link() zamiast połączonych ciągów kotwic, dzięki czemu adresy URL profili i nazwy wyświetlane są odpowiednio przekształcane jako adresy URL i tekst. Ciąg tłumaczenia pozostaje niezmieniony, więc lokalizacje nadal działają. Zniknęło ryzyko ataków XSS na administratora.
Naprawianie filtra „Jakikolwiek błąd oprogramowania” w Traffic Inspector
W dzienniku Traffic Inspector, formularz wyszukiwania zaawansowanego umożliwia łączenie kilku warunków. Jednym z nich jest pole wyboru „Dowolny błąd oprogramowania”. Po połączeniu tego pola wyboru z innymi filtrami, żądania z zarejestrowanym błędem PHP mogły pojawiać się w wynikach, nawet jeśli nie spełniały innych określonych warunków.
Główną przyczyną była kolejność operatorów w sposobie konstruowania klauzuli WHERE. W starszym kodzie fragmenty filtra były łączone jako ciągi znaków, a zgrupowany warunek błędu nie był ujęty w nawiasy, więc operator OR w jego obrębie mógł uniknąć otaczającej go logiki AND . W ramach przeniesienia zapytania o ruch do konstruktora zapytań, zgrupowany warunek błędu jest teraz poprawnie ujęty w nawiasy, a połączony filtr zachowuje się zgodnie z sugestią formularza. Po zawężeniu wyszukiwania wyniki uwzględniają teraz wszystkie ustawione warunki.
Dosłowne dopasowanie symboli wieloznacznych wyszukiwania
Zaawansowane wyszukiwanie w dzienniku ruchu pozwalało wcześniej na to, aby znak % lub _ wpisany w wyszukiwany termin działał jak metaznak LIKE , ponieważ termin ten był umieszczany we wzorcu bez użycia znaku ucieczki. W najlepszym razie jest to przypadkowa funkcja, a w najgorszym niepotrzebny wzorzec obciążenia.
Każdy element LIKE przechodzi teraz przez CRB_Database::escape_like() przed dodaniem otaczających go symboli wieloznacznych, więc % i _ są dopasowywane jako znaki dosłowne wpisane przez użytkownika. Wyszukiwania działają przewidywalnie, a zapytania nie da się zmusić do skanowania bardziej niż zamierzono.
Korygowanie obsługi limitów pamięci dla eksportów
Eksportowanie dużego dziennika aktywności lub ruchu jest bardzo pamięciochłonne, dlatego WP Cerber zwiększa dostępną pamięć przed operacją. W niektórych środowiskach numeryczna wartość limitu pamięci, taka jak 512 była interpretowana jako liczba bajtów, a nie megabajtów. W takim przypadku wtyczka nie zwiększała limitu zgodnie z oczekiwaniami, a eksport mógł zostać zatrzymany wcześniej niż oczekiwano.
Wartość jest teraz interpretowana z użyciem prawidłowej jednostki, dzięki czemu zwiększenie pamięci zostanie zastosowane zgodnie z założeniami, a duże operacje eksportowe zostaną ukończone w środowiskach, na które wcześniej miało to wpływ.
Odbudowa eksportu dziennika wokół strumieniowania
Eksporty aktywności i ruchu służyły do odczytywania pasujących wierszy w blokach, ponownie uruchamiając tę samą SELECT z rosnącym OFFSET dla każdego bloku. W przypadku dużego dziennika, który degraduje się do skanowania z głębokim offsetem, gdzie każdy blok kosztuje więcej niż poprzedni, a wynik jest przechowywany w pamięci.
Oba eksporty odczytują teraz swoje wiersze w jednym, niebuforowanym przebiegu przez CRB_Database::query_stream() , opakowanym w generator, który generuje jeden wiersz na raz. Pamięć pozostaje stała niezależnie od liczby pasujących rekordów, a baza danych wykonuje zadanie raz, a nie raz na blok. Ponieważ niebuforowany strumień blokuje połączenie podczas jego pobierania, suma wierszy i zakres dat są ustalane z góry za pomocą oddzielnych, buforowanych zapytań COUNT i MIN / MAX tworzonych na podstawie tych samych filtrów, przed otwarciem strumienia.
Do współdzielonego pomocnika pobierania plików, crb_file_headers() , dodano dwa nagłówki odpowiedzi. X-Accel-Buffering: no informuje serwer Nginx, gdy jest on frontem PHP-FPM, o przekazywaniu każdego fragmentu danych natychmiast, zamiast buforowania całego eksportu przed jego wysłaniem. Skraca to czas do pierwszego bajtu i zapobiega przechowywaniu dużego pliku CSV w pamięci przez serwer proxy. Nagłówek jest specyficzny dla serwera Nginx i jest ignorowany bez szkody przez serwer Apache z modułem mod_php oraz inne serwery proxy. Cache-Control: no-store zapobiega buforowaniu przez przeglądarkę lub dowolny serwer proxy poufnego eksportu dziennika bezpieczeństwa.
Uczyniliśmy również czyszczenie strumienia deterministycznym. Eksport pobiera dane z generatora w bloku try / finally i zwalnia jedyny pozostały uchwyt w finally , dzięki czemu niebuforowany wynik jest zwalniany, a połączenie odblokowywane na każdej ścieżce wyjścia: przy normalnym zakończeniu, wczesnym zatrzymaniu lub wystąpieniu wyjątku podczas zapisywania wiersza. Wcześniej wczesne przerwanie lub błąd przed pełnym użyciem mogły pozostawić wynik otwarty, a połączenie zablokowane, co powodowało niepowodzenie kolejnego zapytania w tym żądaniu.
Raportowanie zakresu dat eksportu
Nagłówek eksportu CSV wyświetlał tylko aktywne filtry. Zarówno eksport aktywności, jak i ruchu dodają teraz dwa wiersze do nagłówka, pokazujące najstarsze i najnowsze znaczniki czasu rekordów objęte eksportowanymi danymi. Ponieważ nagłówek jest zapisywany przed pierwszym wierszem, zakres pochodzi z towarzyszącego zapytania MIN / MAX utworzonego na podstawie tego samego filtrowanego zapytania co eksport i jest pomijany, gdy żadne wiersze nie pasują. Podczas archiwizacji eksportu plik rejestruje teraz dokładne okno czasowe, które reprezentuje.
Ujawnianie niepowodzeń eksportowych zamiast ich ukrywania
Stara ścieżka eksportu mogła niezauważalnie zawieść. Jeśli baza danych była niedostępna lub nie można było otworzyć strumienia wierszy, poprzedni kod zazwyczaj generował pusty plik CSV bez wyjaśnienia, co jest najgorszym scenariuszem dla osoby próbującej pobrać rekordy podczas incydentu.
Ścieżki odczytu i eksportu dla obu dzienników zostały przeprojektowane, aby zwracać wynik Revalt zawierający albo ładunek danych, albo błąd strukturalny, z odrębnymi kodami, takimi jak activity_export_query_build_failed , activity_export_db_unavailable i activity_export_stream_failed . Błąd niższej warstwy jest łączony z wynikiem, dzięki czemu pierwotna przyczyna zostaje zachowana, a nie odrzucona. Błędy konfiguracji teraz przerywają eksport za pomocą wp_die() przed wysłaniem pojedynczego bajtu CSV, zamiast strumieniować pusty plik.
W przypadku niepowodzenia eksportu administrator z uprawnieniem manage_options widzi powiązaną przyczynę źródłową, na przykład błąd bazy danych, dołączoną do komunikatu. Użytkownicy bez tego uprawnienia nie widzą go, więc szczegółowe informacje dotyczące działania docierają do osób, które mogą na nie zareagować, podczas gdy wewnętrzne szczegóły dotyczące bazy danych pozostają niedostępne dla innych. Komunikat jest kodowany za pomocą crb_escape_html() .
Konsolidacja dzienników aktywności i ruchu w klasach domen
Duża część tego cyklu miała charakter strukturalny. Zarówno dziennik aktywności, jak i dziennik ruchu zawierały ciągi SQL i obsługę wyników rozproszone po kodzie pulpitu nawigacyjnego i eksportu. Przenieśliśmy tę logikę do klas CRB_Activity i CRB_Traffic_Log , więc budowanie zapytań i pobieranie wierszy odbywa się teraz za pomocą przejrzystych metod, takich jak fetch() i stream_log() zamiast przechodzić przez kod prezentacji jako surowy kod SQL.
Obie klasy budują teraz swoje zapytania za pomocą konstruktora zapytań DB Warp, uzyskanego za pomocą warp_get_db() zamiast ręcznie łączyć fragmenty WHERE, JOIN i LIMIT. Ma to znaczenie wykraczające poza kwestię porządku. Kierowanie każdej wartości filtru dostarczonej przez użytkownika przez jedną warstwę ucieczki eliminuje wcześniejsze połączenie ręcznej ucieczki, $wpdb->prepare() i ręcznej konkatenacji w cudzysłowie, co jest dokładnie tym rodzajem niespójności, która ukrywa błędy iniekcji. Gdy zdegradowana warstwa bazy danych nie może zbudować zapytania, kod teraz kończy się niepowodzeniem z warunkiem braku dopasowania, zamiast uruchamiać niefiltrowane zapytanie.
Te zmiany są wewnętrzne i nie zmieniają tego, co jest wyświetlane na ekranach, ale stanowią podstawę, na której oparto powyższe poprawki dotyczące bezpieczeństwa i niezawodności — są one drobne, lokalne i weryfikowalne.
Znaleziono dwa ukryte błędy podczas wyodrębniania kodu alertu
Przeniesienie wysyłania alertów administracyjnych z CRB_Activity::log() do dedykowanej klasy CRB_Activity_Alerts spowodowało pojawienie się dwóch istniejących wcześniej błędów, które były spowodowane przez tablicę pozycyjną, której indeksy numeryczne nie były zgodne.
Pierwszy dotyczył linków w panelu w alertach e-mail. Parowanie rzadkiego klucza i gęstej wartości spowodowało przesunięcie wartości, przez co parametr linku, taki jak filter_ip mógł otrzymać początek zakresu adresów IP zamiast zamierzonego adresu. Mapowanie wartości na nazwane klucze naprawiło wyrównanie, a linki w alertach e-mail teraz wskazują tam, gdzie powinny.
Drugi problem dotyczył dopasowania użytkownika do ciągu wyszukiwania. Kod o nazwie wp_get_current_user() zwraca obiekt WP_User nawet dla identyfikatora użytkownika 0, więc dopasowanie używało niewłaściwej tożsamości. Teraz wyszukuje użytkownika zdarzenia za pomocą crb_get_userdata() i chroni przed brakiem użytkownika. Alerty, które pasują do użytkownika, teraz pasują do poprawnego.
Cichsza tablica rozdzielcza: pierwszeństwo operatora w sprawdzaniu modyfikacji
CRB_Activity::is_modified_since() porównywał znacznik czasu za pomocą $stamp < $status['data_modified'] ?? PHP_INT_MAX . Kolejność operatorów wiąże to jako (... < ...) ?? PHP_INT_MAX , co powodowało, że część łącząca wartości null była martwym kodem i zgłaszała ostrzeżenie o niezdefiniowanym kluczu za każdym razem, gdy data_modified była nieobecna. Funkcja łączenia jest teraz ujęta w nawiasy, więc brakujący znacznik modyfikacji jest traktowany jako „zmodyfikowany”, pasujący do siostrzanej metody is_modified() , a fałszywe ostrzeżenie znika.
Centralizowanie definicji schematów bazy danych
Ostatnim elementem strukturalnym jest utrzymanie schematu. Kod instalacji i aktualizacji wykorzystywał wbudowany kod CREATE TABLE SQL dla tabel logów, co oznaczało, że tę samą tabelę można było opisać w więcej niż jednym miejscu. Deklaracje te pochodzą teraz z jednego źródła, CRB_Schema_Definitions , dzięki czemu instalacje i aktualizacje działają na podstawie jednej kanonicznej definicji, a CRB_Schema_Manager może bardziej spójnie wykrywać odchylenia schematu w istniejących instalacjach. Surowy DROP INDEX SQL został również zastąpiony przez CRB_Schema_Manager::drop_index_if_exists() .
Uprościliśmy również dekodowanie przechowywanych danych pól żądań. Wartości null, wartości puste, nieprawidłowe dane JSON i nieobsługiwane serializowane dane są teraz obsługiwane w ten sam bezpieczny sposób, poprzez rozwiązywanie do pustej tablicy, zamiast pozwalania, aby błędnie sformatowany rekord zakłócił odczyt.
Dlaczego to wydanie jest ważne
W wersji 9.8.3 nie ma nowych przycisków. Zamiast tego wprowadzono warstwę danych, która działa niezawodnie, a nie w sposób cichy, powiadomienie e-mail, którym nie można sterować za pomocą zmodyfikowanej nazwy profilu, powiadomienie administratora, które poprawnie ukrywa dane zewnętrzne, eksporty logów działające w pamięci płaskiej i informujące o błędach oraz filtry wyszukiwania, które działają dokładnie tak, jak nakazuje formularz. To właśnie tego typu zmiany sprawiają, że wtyczka zabezpieczająca jest godna zaufania w dłuższej perspektywie, a my wolimy działać otwarcie, niż udawać, że wcześniejszy kod był już idealny.