Nette Documentation Preview

syntax
Ochrona przed SSRF
******************

.[perex]
Gdy Twoja aplikacja pobiera URL podany przez użytkownika, atakujący może to wykorzystać, żeby dosięgnąć Twojej sieci wewnętrznej. Klasy [#UrlValidator] i [#IPAddress] pomagają Ci bronić się przed tymi atakami Server-Side Request Forgery (SSRF).

→ [Instalacja i wymagania |@home#Instalacja]


Czym jest SSRF?
===============

Wyobraź sobie funkcję, w której użytkownik wpisuje URL, a Twój serwer go pobiera: awatar spod zdalnego adresu, cel webhooka, podgląd odnośnika. Wygląda niewinnie, ale pod adres sięga serwer, a nie przeglądarka użytkownika. A serwer widzi miejsca, których atakujący nie widzi: interfejs loopback, sieć prywatną, usługi chmurowe.

Atakujący podaje więc URL wskazujący do wewnątrz zamiast do publicznego internetu. Typowe cele to:

- metadane chmury pod `http://169.254.169.254/`, z których mogą wyciec klucze dostępowe
- wewnętrzne panele administracyjne i routery, jak `http://192.168.1.1/`
- usługi bez uwierzytelniania, na przykład Redis pod `http://localhost:6379/`

Ta klasa podatności jest tak powszechna, że plasuje się w [OWASP Top 10 |https://owasp.org/Top10/]. Obroną jest zwalidowanie URL **przed** jego pobraniem i odrzucenie wszystkiego, co rozwiązuje się do adresu niepublicznego.


UrlValidator
============

[api:Nette\Http\UrlValidator] sprawdza URL względem konfigurowalnej polityki: schematu, portu, hosta, userinfo i adresów IP, do których host się rozwiązuje. Podstawowe użycie to jedno wywołanie:

```php
use Nette\Http\UrlValidator;

if (!(new UrlValidator)->allows($userUrl)) {
	return; // niebezpieczny URL, nie pobieraj go
}
```

Domyślna polityka jest celowo surowa: przyjmuje wyłącznie `https` na porcie 443 wskazujące na publiczny adres IP. Wszystko inne (loopback, zakresy prywatne, link-local wraz z metadanymi chmury, zakresy zarezerwowane) jest odrzucane, a multicast odrzucany jest bezwarunkowo. To właściwy punkt wyjścia przy pobieraniu dowolnych URL-i podanych przez użytkownika.


Konfiguracja polityki
---------------------

Politykę kształtujesz przez konstruktor. Na przykład żeby dopuścić zwykłe `http` na dowolnym porcie i sięgać po adresy prywatne (przydatne wewnątrz zaufanej sieci):

```php
$validator = new UrlValidator(
	schemes: ['http', 'https'],
	ports: null, // dowolny port
	allowPrivateIps: true,
);
```

Częstym wzorcem jest ograniczenie pobierania do ustalonego zbioru domen partnerskich za pomocą whitelisty hostów. Przedrostek `*.` dopasowuje subdomeny dowolnej głębokości, ale nie samą domenę: jeśli tego potrzebujesz, wypisz obie formy:

```php
$validator = new UrlValidator(
	hostAllowlist: ['example.com', '*.example.com'],
);
```

Pełny zestaw opcji konstruktora:

| Parametr | Domyślnie | Znaczenie
|---------------------
| `schemes` | `['https']` | dozwolone schematy; `[]` odrzuca wszystko
| `ports` | `[443]` | dozwolone porty, `null` = dowolny; niejawny port ze schematu jest respektowany
| `allowPrivateIps` | `false` | dopuszcza zakresy prywatne (10/8, 172.16/12, 192.168/16, fc00::/7)
| `allowLoopback` | `false` | dopuszcza loopback (127.0.0.0/8, ::1)
| `allowLinkLocal` | `false` | dopuszcza link-local wraz z metadanymi chmury 169.254.169.254
| `allowReserved` | `false` | dopuszcza zakresy zarezerwowane przez IANA
| `allowUserinfo` | `false` | dopuszcza `user:pass@` w URL
| `hostAllowlist` | `null` | jeśli ustawione, host musi pasować do jednego ze wzorców; `[]` odrzuca wszystkie
| `hostBlocklist` | `null` | jeśli ustawione, host nie może pasować do żadnego wzorca


Metody walidacyjne
------------------

Walidator oferuje trzy metody. `allows()` przeprowadza pełną kontrolę wraz z rozwiązaniem DNS: host jest rozwiązywany i **każdy** adres A/AAAA musi przejść politykę IP:

```php
(new UrlValidator)->allows($url); // bool
```

`allowsWithoutDns()` pomija rozwiązanie DNS i kontrole zakresów IP. Użyj jej jako szybkiego filtra wstępnego albo wtedy, gdy walidacja DNS jest delegowana do warstwy pobierającej:

```php
(new UrlValidator)->allowsWithoutDns($url); // bool
```

Obie metody przyjmują ciąg, obiekt [UrlImmutable |urls#UrlImmutable] albo `null` (który zawsze nie przechodzi).


Pokonanie DNS rebindingu
------------------------

Między walidacją a pobraniem istnieje subtelny wyścig: atakujący może zwrócić bezpieczne IP, gdy walidujesz host, a potem przełączyć DNS na wewnętrzne IP przy właściwym pobraniu. Żeby zamknąć tę lukę, `getResolvedIPs()` zwraca zwalidowane adresy IP, a Ty przypinasz do nich połączenie, żeby pobrania nie dało się przekierować gdzie indziej:

```php
$ips = (new UrlValidator)->getResolvedIPs($url);
if (!$ips) {
	return; // niebezpieczny URL
}

$ch = curl_init($url);
$host = parse_url($url, PHP_URL_HOST);
curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]);
// ... wykonanie żądania
```

Metoda zwraca tablicę ciągów IP (najpierw rekordy A, potem AAAA), które przeszły pełną politykę, albo pustą tablicę przy jakimkolwiek niepowodzeniu. Dla literału IP w URL waliduje adres bezpośrednio i nie wykonuje zapytania DNS.


IPAddress
=========

[api:Nette\Http\IPAddress] to niezmienny obiekt wartości do pracy z adresami IPv4 i IPv6. `UrlValidator` używa go wewnętrznie, ale przydaje się też sam w sobie zawsze, gdy klasyfikujesz adresy. Konstruktor rzuca `Nette\InvalidArgumentException` dla nieprawidłowego adresu:

```php
use Nette\Http\IPAddress;

$ip = new IPAddress('169.254.169.254');
echo $ip; // '169.254.169.254'
```

Gdy nie chcesz wyjątku, użyj fabryki `tryFrom()` albo testu `isValid()`:

```php
$ip = IPAddress::tryFrom($input); // ?IPAddress
IPAddress::isValid($input);       // bool
```


Klasyfikacja adresów
--------------------

Predykaty mówią Ci, do której klasy należy adres. Kluczowy jest `isPublic()`: true tylko dla adresów publicznie routowalnych, czyli dokładnie tego, czego chce ochrona przed SSRF:

```php
$ip = new IPAddress('169.254.169.254');
$ip->isPublic();    // false
$ip->isLinkLocal(); // true (zakres metadanych chmury)
```

Pełny zestaw predykatów:

| Metoda | Testuje
|--------------------
| `isPublic()` | publicznie routowalny (żaden z poniższych)
| `isPrivate()` | zakresy prywatne RFC 1918 / 4193
| `isLoopback()` | 127.0.0.0/8, ::1
| `isLinkLocal()` | 169.254.0.0/16 (wraz z metadanymi chmury), fe80::/10
| `isMulticast()` | 224.0.0.0/4, ff00::/8
| `isReserved()` | zarezerwowane przez IANA (dokumentacja, CGNAT, przyszłe użycie, …)


Przynależność do zakresu
------------------------

`isInRange()` testuje, czy adres mieści się w bloku CIDR. Możesz przekazać sieć z prefiksem albo goły adres dla dokładnego dopasowania (niejawne /32 dla IPv4, /128 dla IPv6):

```php
$ip = new IPAddress('192.168.1.50');
$ip->isInRange('192.168.0.0/16'); // true
$ip->isInRange('10.0.0.1');       // false (dokładne dopasowanie)
```

Zniekształcone wejście albo inna rodzina IP zwraca `false`.


IPv6 z mapowanym IPv4
---------------------

Adresy zapisane jako IPv6 z mapowanym IPv4 (jak `::ffff:127.0.0.1`) to klasyczny sposób na prześlizgnięcie się obok naiwnych filtrów. `IPAddress` je normalizuje, więc predykaty zakresów przejrzą to przebranie:

```php
$ip = new IPAddress('::ffff:127.0.0.1');
$ip->isLoopback();   // true
$ip->isIPv4Mapped(); // true
$ip->toIPv4();       // IPAddress('127.0.0.1')
```

Metody `isIPv4()` i `isIPv6()` raportują postać tekstową: adres mapowany jest IPv6, a nie IPv4.

Ochrona przed SSRF

Gdy Twoja aplikacja pobiera URL podany przez użytkownika, atakujący może to wykorzystać, żeby dosięgnąć Twojej sieci wewnętrznej. Klasy UrlValidatorIPAddress pomagają Ci bronić się przed tymi atakami Server-Side Request Forgery (SSRF).

Instalacja i wymagania

Czym jest SSRF?

Wyobraź sobie funkcję, w której użytkownik wpisuje URL, a Twój serwer go pobiera: awatar spod zdalnego adresu, cel webhooka, podgląd odnośnika. Wygląda niewinnie, ale pod adres sięga serwer, a nie przeglądarka użytkownika. A serwer widzi miejsca, których atakujący nie widzi: interfejs loopback, sieć prywatną, usługi chmurowe.

Atakujący podaje więc URL wskazujący do wewnątrz zamiast do publicznego internetu. Typowe cele to:

  • metadane chmury pod http://169.254.169.254/, z których mogą wyciec klucze dostępowe
  • wewnętrzne panele administracyjne i routery, jak http://192.168.1.1/
  • usługi bez uwierzytelniania, na przykład Redis pod http://localhost:6379/

Ta klasa podatności jest tak powszechna, że plasuje się w OWASP Top 10. Obroną jest zwalidowanie URL przed jego pobraniem i odrzucenie wszystkiego, co rozwiązuje się do adresu niepublicznego.

UrlValidator

Nette\Http\UrlValidator sprawdza URL względem konfigurowalnej polityki: schematu, portu, hosta, userinfo i adresów IP, do których host się rozwiązuje. Podstawowe użycie to jedno wywołanie:

use Nette\Http\UrlValidator;

if (!(new UrlValidator)->allows($userUrl)) {
	return; // niebezpieczny URL, nie pobieraj go
}

Domyślna polityka jest celowo surowa: przyjmuje wyłącznie https na porcie 443 wskazujące na publiczny adres IP. Wszystko inne (loopback, zakresy prywatne, link-local wraz z metadanymi chmury, zakresy zarezerwowane) jest odrzucane, a multicast odrzucany jest bezwarunkowo. To właściwy punkt wyjścia przy pobieraniu dowolnych URL-i podanych przez użytkownika.

Konfiguracja polityki

Politykę kształtujesz przez konstruktor. Na przykład żeby dopuścić zwykłe http na dowolnym porcie i sięgać po adresy prywatne (przydatne wewnątrz zaufanej sieci):

$validator = new UrlValidator(
	schemes: ['http', 'https'],
	ports: null, // dowolny port
	allowPrivateIps: true,
);

Częstym wzorcem jest ograniczenie pobierania do ustalonego zbioru domen partnerskich za pomocą whitelisty hostów. Przedrostek *. dopasowuje subdomeny dowolnej głębokości, ale nie samą domenę: jeśli tego potrzebujesz, wypisz obie formy:

$validator = new UrlValidator(
	hostAllowlist: ['example.com', '*.example.com'],
);

Pełny zestaw opcji konstruktora:

Parametr Domyślnie Znaczenie
schemes ['https'] dozwolone schematy; [] odrzuca wszystko
ports [443] dozwolone porty, null = dowolny; niejawny port ze schematu jest respektowany
allowPrivateIps false dopuszcza zakresy prywatne (10/8, 172.16/12, 192.168/16, fc00::/7)
allowLoopback false dopuszcza loopback (127.0.0.0/8, ::1)
allowLinkLocal false dopuszcza link-local wraz z metadanymi chmury 169.254.169.254
allowReserved false dopuszcza zakresy zarezerwowane przez IANA
allowUserinfo false dopuszcza user:pass@ w URL
hostAllowlist null jeśli ustawione, host musi pasować do jednego ze wzorców; [] odrzuca wszystkie
hostBlocklist null jeśli ustawione, host nie może pasować do żadnego wzorca

Metody walidacyjne

Walidator oferuje trzy metody. allows() przeprowadza pełną kontrolę wraz z rozwiązaniem DNS: host jest rozwiązywany i każdy adres A/AAAA musi przejść politykę IP:

(new UrlValidator)->allows($url); // bool

allowsWithoutDns() pomija rozwiązanie DNS i kontrole zakresów IP. Użyj jej jako szybkiego filtra wstępnego albo wtedy, gdy walidacja DNS jest delegowana do warstwy pobierającej:

(new UrlValidator)->allowsWithoutDns($url); // bool

Obie metody przyjmują ciąg, obiekt UrlImmutable albo null (który zawsze nie przechodzi).

Pokonanie DNS rebindingu

Między walidacją a pobraniem istnieje subtelny wyścig: atakujący może zwrócić bezpieczne IP, gdy walidujesz host, a potem przełączyć DNS na wewnętrzne IP przy właściwym pobraniu. Żeby zamknąć tę lukę, getResolvedIPs() zwraca zwalidowane adresy IP, a Ty przypinasz do nich połączenie, żeby pobrania nie dało się przekierować gdzie indziej:

$ips = (new UrlValidator)->getResolvedIPs($url);
if (!$ips) {
	return; // niebezpieczny URL
}

$ch = curl_init($url);
$host = parse_url($url, PHP_URL_HOST);
curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]);
// ... wykonanie żądania

Metoda zwraca tablicę ciągów IP (najpierw rekordy A, potem AAAA), które przeszły pełną politykę, albo pustą tablicę przy jakimkolwiek niepowodzeniu. Dla literału IP w URL waliduje adres bezpośrednio i nie wykonuje zapytania DNS.

IPAddress

Nette\Http\IPAddress to niezmienny obiekt wartości do pracy z adresami IPv4 i IPv6. UrlValidator używa go wewnętrznie, ale przydaje się też sam w sobie zawsze, gdy klasyfikujesz adresy. Konstruktor rzuca Nette\InvalidArgumentException dla nieprawidłowego adresu:

use Nette\Http\IPAddress;

$ip = new IPAddress('169.254.169.254');
echo $ip; // '169.254.169.254'

Gdy nie chcesz wyjątku, użyj fabryki tryFrom() albo testu isValid():

$ip = IPAddress::tryFrom($input); // ?IPAddress
IPAddress::isValid($input);       // bool

Klasyfikacja adresów

Predykaty mówią Ci, do której klasy należy adres. Kluczowy jest isPublic(): true tylko dla adresów publicznie routowalnych, czyli dokładnie tego, czego chce ochrona przed SSRF:

$ip = new IPAddress('169.254.169.254');
$ip->isPublic();    // false
$ip->isLinkLocal(); // true (zakres metadanych chmury)

Pełny zestaw predykatów:

Metoda Testuje
isPublic() publicznie routowalny (żaden z poniższych)
isPrivate() zakresy prywatne RFC 1918 / 4193
isLoopback() 127.0.0.0/8, ::1
isLinkLocal() 169.254.0.0/16 (wraz z metadanymi chmury), fe80::/10
isMulticast() 224.0.0.0/4, ff00::/8
isReserved() zarezerwowane przez IANA (dokumentacja, CGNAT, przyszłe użycie, …)

Przynależność do zakresu

isInRange() testuje, czy adres mieści się w bloku CIDR. Możesz przekazać sieć z prefiksem albo goły adres dla dokładnego dopasowania (niejawne /32 dla IPv4, /128 dla IPv6):

$ip = new IPAddress('192.168.1.50');
$ip->isInRange('192.168.0.0/16'); // true
$ip->isInRange('10.0.0.1');       // false (dokładne dopasowanie)

Zniekształcone wejście albo inna rodzina IP zwraca false.

IPv6 z mapowanym IPv4

Adresy zapisane jako IPv6 z mapowanym IPv4 (jak ::ffff:127.0.0.1) to klasyczny sposób na prześlizgnięcie się obok naiwnych filtrów. IPAddress je normalizuje, więc predykaty zakresów przejrzą to przebranie:

$ip = new IPAddress('::ffff:127.0.0.1');
$ip->isLoopback();   // true
$ip->isIPv4Mapped(); // true
$ip->toIPv4();       // IPAddress('127.0.0.1')

Metody isIPv4() i isIPv6() raportują postać tekstową: adres mapowany jest IPv6, a nie IPv4.