Filtrowanie danych w PHP

Udostępniając formularz na stronie internetowej, bądź przykładowo API dajemy użytkownikom możliwość wysłania do nas dowolnych danych. Zresztą samo posiadanie strony jest już czynnikiem ryzyka związanym z atakami, bo różne skrypty na stronie wysyłają i przyjmują różne zapytania z zewnątrz.

Jedną z ważnych elementów tworzenia jakichkolwiek aplikacji jest walidacja, bądź sanityzacja danych. W skrócie polega ona na tym, że zanim zapiszemy cokolwiek w bazie, bądź wykonamy operacje na jakichkolwiek danych przekazanych pośrednio lub bezpośrednio przez użytkownika, powinniśmy sprawdzić czy aby na pewno ten wysłał to co założyliśmy.

Przykładowo – jeżeli udostępniamy na stronie formularz w którym użytkownik wpisuje adres e-mail to istotna jest weryfikacja czy aby na pewno użytkownik podał adres e-mail., a nie kod w SQL lub cokolwiek innego. Wyobraźmy sobie taki scenariusz.

  1. Udostępniamy użytkownikowi możliwość wpisania adresu e-mail w formularzu.
  2. Następnie zapisujemy adres email w taki sposób (w zmiennej $email mamy to co wpisał użytkownik) INSERT INTO users (email) VALUES ('$email')
  3. Dowcipny użytkownik jednak zamiast prawidłowego adresu e-mail wpisuje '); DELETE FROM users; --
  4. To powoduje, że wykonujemy taką komendę: INSERT INTO users (email) VALUES (''); DELETE FROM users; --')
  5. -- to komentarz w SQL, więc to co jest po nim nie będzie brane pod uwagę, a cała operacje może skutkować usunięciem wszystkich użytkowników w bazie.

Łatwo sobie wyobrazić, że w ten sposób można bardzo dużo – dodać się do bazy, pobierać dane itd.

Filtry

PHP udostępnia zestaw dedykowanych funkcji do filtrowania danych. Dzielą się na dwa typy:

  • walidujące, czyli sprawdzanie czy dane są takie jak być powinny
  • sanityzujące, czyli sprawiające by dany były takie jakich oczekujemy (np. usunięcie niedozwolonych znaków itp.)

Do obsługi obu typów służy funkcja filter_var.

filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed

$value – wartość, którą sprawdzamy / poprawiamy

$filter – jeden z kilkudziesięciu dostępnych filtrów np. e-mail, adres strony internetowej itd.

$options – dodatkowe opcje lub flagi np. zezwól na polskie znaki w adresie e-mail. Przekazujemy je w tablicy z kluczem options i flags. Dodatkowo dostępna jest opcja default, która pozwala na zwrócenie dowolnej wartości zamiast false.

Przykład użycia

$params = [
  'options' => [
    'default' => 0,
    'min_range' => 1,
    'max_range' => 1000
  ],
  'flags' => FILTER_FLAG_ALLOW_HEX | FILTER_FLAG_ALLOW_OCTAL
];

filter_var ('555.44', FILTER_VALIDATE_INT, array|int $options = 0): mixed

Istnieje jeszcze tablicowa odmiana funkcji – filter_var_array, która przyjmuje tablicę wartości, a nie pojedynczą wartość.

Filtry walidujące

Filtry walidujące sprawdzają wartość i zwracają true (prawidłowa), false (nieprawidłowa) lub null (jeżeli jest ustawiona flaga w opcjach FILTER_NULL_ON_FAILURE. Idea z tą flagą jest taka, że sprawdzany jest format zwracanych danych np. jak oczekujemy wartości „true”, „1”, „on”, „false”, „0”, „off” to otrzymamy null, jeżeli żadna z tych wartości nie zostanie zwrócona. Bez flagi otrzymalibyśmy zawsze false. Poza flagą niektóre filtry mają również dodatkowe opcje (np. możliwość ustawienia minimalnej i maksymalnej wartości) oraz opcję default w której przekazujemy co ma zwrócić funkcja zamiast false.

  • FILTER_VALIDATE_EMAIL – sprawdza czy adres e-mail ma prawidłowy format. Dostępna jest flaga FILTER_FLAG_EMAIL_UNICODE, która zezwala na używanie np. polskich znaków w adresie.
  • FILTER_VALIDATE_URL – sprawdza czy adres URL ma prawidłowy format. Dodatkowo możemy ustawić flagi FILTER_FLAG_PATH_REQUIRED oraz FILTER_FLAG_QUERY_REQUIRED, które sprawdzają odpowiednio czy adres zwiera ścieżkę (znak / np. example.com/index.php) oraz zapytanie (? np. index.php?query=test).
  • FILTER_VALIDATE_DOMAIN – sprawdza czy adres domeny ma prawidłowy format. Włączenie flagi FILTER_FLAG_HOSTNAME dodatkowo sprawdza czy zaczyna się od litery alfabetu lub cyfry oraz zawiera tylko takie znaki lub łączniki (np. -)
  • FILTER_VALIDATE_IP – sprawdza czy wartość jest adresem IP. Przy użyciu flag możemy ograniczyć zakres sprawdzania adresu: FILTER_FLAG_IPV4FILTER_FLAG_IPV6FILTER_FLAG_NO_PRIV_RANGE, FILTER_FLAG_NO_RES_RANGE, FILTER_FLAG_GLOBAL_RANGE,
  • FILTER_VALIDATE_MAC – sprawdza czy wartość jest adresem MAC.
  • FILTER_VALIDATE_BOOL – zwraca true dla wartości 1, "true" lub "on" – przydatne jak korzystamy np. z JavaScript do przesyłania danych typu boolean
  • FILTER_VALIDATE_INT – sprawdza czy wartość jest typu int. W opcjach możemy ustawić minimalną (min_range) i maksymalną (max_range) wartość. Dodatkowe flagi pozwalają też na przekazanie wartości w systemie szesnastkowym (FILTER_FLAG_ALLOW_HEX)lub ósemkowym (FILTER_FLAG_ALLOW_OCTAL)
  • FILTER_VALIDATE_FLOAT – sprawdza czy wartość jest typu float. W opcjach możemy dodatkowo ustawić minimalną (min_range) i maksymalną (max_range) wartość oraz znak rozdzielający część ułamkową np. przecinek zamiast kropki (opcja decimal). Ponadto dostępna jest flaga FILTER_FLAG_ALLOW_THOUSAND, która pozwala na rozdzielanie części tysięcznych np. możliwy wtedy jest taki zapis 13,111,753.479.
  • FILTER_VALIDATE_REGEXP – sprawdza czy adres pasuje do wyrażenia regularnego. Wyrażenie podajemy w opcji regexp.

Filtry sanityzujące

Filtry sanityzujące służą do poprawienia danych (np. usuwają zbędne znaki) i zwracają zmienione dane, które potem możemy np. poddać pod filtr walidujący. Korzysta się z nich tak samo jak z filtrów walidujących z tą różnicą, że nie ma tutaj dostępnych opcji – są tylko dodatkowe flagi. Na końcu wyjaśnimy niektóre flagi, które pojawiają się w wielu funkcjach.

  • FILTER_DEFAULT, FILTER_UNSAFE_RAW – domyślny filtr, który nie robi nic, ale umożliwia ustawienie dodatkowych flag. które omówimy na końcu FILTER_FLAG_STRIP_LOW, FILTER_FLAG_STRIP_HIGH, FILTER_FLAG_STRIP_BACKTICK, FILTER_FLAG_ENCODE_LOW, FILTER_FLAG_ENCODE_HIGH, FILTER_FLAG_ENCODE_AMP
  • FILTER_SANITIZE_EMAIL – usuwa wszystkie znaki specjalne oprócz dozwolonych w adresie e-mail
  • FILTER_SANITIZE_ENCODED – usuwa lub zamienia znaki w adresie url, działa podobnie do urlencode(). Dostępne flagi to FILTER_FLAG_STRIP_LOW, FILTER_FLAG_STRIP_HIGH, FILTER_FLAG_STRIP_BACKTICK, FILTER_FLAG_ENCODE_LOW, FILTER_FLAG_ENCODE_HIGH
  • FILTER_SANITIZE_ADD_SLASHES – dodaje ukośniki przed znakami ' , "" , \, null. Działa tak jak addslashes.
  • FILTER_SANITIZE_NUMBER_FLOAT – usuwa wszystkie znaki za wyjątkiem cyfr i znaków +-. Dostępne flagi pozwalają na zwiększenie zakresu: FILTER_FLAG_ALLOW_FRACTION – kropka jako znak rozdzielający część ułamkową, FILTER_FLAG_ALLOW_THOUSAND – rozdzielenie części tysięcznych przecinkiem, FILTER_FLAG_ALLOW_SCIENTIFIC – używanie e lub E jak naukowa notacja
  • FILTER_SANITIZE_NUMBER_INT – usuwa wszystkie znaki za wyjątkiem cyfr i znaków plus i minus.
  • FILTER_SANITIZE_SPECIAL_CHARS – zamienia znaki '"<>& i znaki sterujące na format HTML-a. Dostępne filtry: FILTER_FLAG_STRIP_LOW, FILTER_FLAG_STRIP_HIGH, FILTER_FLAG_STRIP_BACKTICK, FILTER_FLAG_ENCODE_HIGH
  • FILTER_SANITIZE_FULL_SPECIAL_CHARS – zamienia znaki '"<>& i znaki sterujące na format HTML-a. Działa tak jak funkcja htmlspecialchars z włączoną flagą ENT_QUOTES. W przypadku wykrycia znaku spoza domyślnie ustawionego kodowania zwrócony zostanie pusty łańcuch. Filtr FILTER_FLAG_NO_ENCODE_QUOTES powoduje, że znaki ' i "" nie będą kodowane. Na serwerach często ta flaga jest włączona domyślnie.
  • FILTER_SANITIZE_URL – usuwa wszystkie znaki oprócz liter, cyfr i używanych w adresie url.

Niektóre flagi są używane przez wiele funkcji. Flagi ze STRIP w nazwie – usuwają, a te z ENCODE w nazwie – zamieniają.

  • FILTER_FLAG_STRIP_LOW, FILTER_FLAG_ENCODE_LOW – usuwa / koduje znaki sterujące (ASCII < 32)
  • FILTER_FLAG_STRIP_HIGH, FILTER_FLAG_ENCODE_HIGH – usuwa / koduje znaki z górnej połowy tabeli ASCI (ASCII > 127)
  • FILTER_FLAG_STRIP_BACKTICK, FILTER_FLAG_ENCODE_BACKTICK – usuwa / koduje znak cofania.

Filtrowanie odpowiedzi z serwera

Do filtrowania danych przychodzących może posłużyć funkcja filter_input, która w pierwszym parametrze przyjmuje typ danych, a w drugim nazwę pola. W kolejnych parametrach podajemy filtr i opcje (jak w poprzednich funkcjach).

  • INPUT_GET – sprawdzana jest zmienna globalna http $_GET
  • INPUT_POST – sprawdzana jest zmienna globalna http $_POST
  • INPUT_COOKIE – sprawdzana jest zmienna globalna ciasteczek $_COOKIE
  • INPUT_SERVER – sprawdzana jest zmienna globalna serwera $_SERVER
  • INPUT_ENV – sprawdzana jest środowiskowa zmienna globalna $_ENV

Przykładowo dla adresu http://example.com?test=1 , filter_input(INPUT_GET, 'test') zwróci domyślnie wartość true. Oczywiście możemy stosować filtry i opcje tak jak w poprzedniej funkcji i uzyskać poprawiony łańcuch. Jeżeli zmienna nie istnieje zostanie zwrócona wartość null.

Podobnie jak poprzednio, dostępny jest tablicowy odpowiednik funkcji – filter_input_array, który przyjmuje tablicę w pierwszym argumencie.

Walidacja z użyciem tablicowego odpowiednika

Funkcja filter_var_array pozwala na filtrowanie wielu danych jednocześnie.

Możemy ustawić jedną flagę dla wszystkich danych przekazanych w tablicy, albo ustawić tablicę z filtrami, która będzie odpowiadała konkretnym danym.

filter_var_array(
  [ 'nick' => 'Eskim',
    'age' => 199,
    'email' => 'email@example.com',
  ],
  [ 'nick' => [
      'filter' => FILTER_SANITIZE_SPECIAL_CHARS,
      'flags' => FILTER_FLAG_STRIP_BACKTICK | FILTER_FLAG_ENCODE_HIGH,
    ],
    'age' => [
      'filter' => FILTER_VALIDATE_INT,
      'flags' => FILTER_FLAG_STRIP_BACKTICK | FILTER_FLAG_ENCODE_HIGH,
      'options' => ['min_range' => 3, 'max_range' => 150]
    ],
    'email' => [
      'filter' => FILTER_VALIDATE_EMAIL
    ],
  ]
]
);

Zapytania SQL

Przed wysłaniem zapytania do bazy powinniśmy również podać je sanityzacji. Do tego służy specjalna funkcja prepare, która występuje chyba we wszystkich bibliotekach do obsługi zapytań w PHP (choć oczywiście może się nieco różnić składnią). Najpierw używamy prepare, a potem dopiero wykonujemy zapytanie. Implementacja tej funkcjonalności różni się w zależności od biblioteki, ale cel pozostaje ten sam.

PDO

PDO jest najpopularniejszą biblioteką PHP używaną do operowania na bazie danych. Zanim wyślemy zapytanie zamieniamy je na obiekt i przed wysłaniem (lub w trakcie) wpisujemy parametry do obiektu.

$prep = $db->prepare (' SELECT id FROM users WHERE name = :username');
$prep->bindParam('username', $username, PDO::PARAM_STR);
$prep->execute();

W powyższym przykładzie tworzy zapytanie z parametrem username pod który później podpinamy zmienną typu string $username. Robimy to za pomocą bindParam. Oczywiście zmiennych może być więcej. Parametry można także przekazać bezpośrednio przy wywoływaniu metody execute.

HTML Purifier

HTML Purifier to biblioteka filtrująca napisana w PHP, która służy do oczyszczania kodu HTML usuwając potencjalnie niebezpieczne treści i upewniając się, że wynikowy kod HTML jest poprawny z punktu widzenia norm i standardów. Biblioteka jest zaprojektowana w taki sposób, aby chronić przed różnego rodzaju atakami, takimi jak ataki XSS (Cross-site Scripting):

  • oczyszcza kod, usuwając niebezpieczne tagi, atrybuty i skrypty,
  • upewnia się, że wynikowy kod HTML jest zgodny ze standardami określonymi przez W3C

Możesz dostosować działanie biblioteki do swoich potrzeb wybierając, które tagi czy atrybuty chcesz zachować, a które usunąć. Potrafi również przekształcać niektóre tagi lub atrybuty w inne na przykład przekształcając wszystkie odnośniki target="_blank" w rel="noopener", aby były bardziej bezpieczne. Biblioteka obsługuje nie tylko fragmenty kodu HTML, ale również całe dokumenty, w tym DOCTYPE, HTML i BODY.

Jeśli planujesz osadzać treści użytkowników na swojej stronie internetowej lub aplikacji, HTML Purifier to jedno z narzędzi, które warto rozważyć w celu zapewnienia bezpieczeństwa i zgodności wynikowego kodu HTML.

Co dalej?

Powyższe nie wyczerpuje tematu, który jest znacznie szerszy i obejmuje każdy rodzaj danych, który pobieramy z dowolnego zewnętrznego źródła – pliku, przez internet, itd. Najważniejsze, by zawsze o tym pamiętać i zakładać najgorszy scenariusz.

Podsumowanie

  1. Dane uzyskane od użytkownika powinny być filtrowane.
  2. Sanityzację i walidację danych można przeprowadzić wykorzystując funkcję filter_var lub jej tablicowego odpowiednika filter_var_array.
  3. Filtry walidujące sprawdzają poprawność podanych danych np. adres e-mail.
  4. Filtry sanityzujące poprawiają dane na format, który nas interesuje np. usuwają znaki specjalne.
  5. Do filtrowania danych przychodzących możemy skorzystać z funkcji filter_input.
  6. Biblioteki do obsługi baz danych oferują zwykle dedykowane funkcje do sanityzacji danych.