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 UrlValidator i IPAddress 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.