NGINX: Poprawne przekazywanie nagłówka Host

27 Mar 2019

best-practices  headers  host  http  nginx  proxy_pass 

Share on:

Nagłówek Host jest jednym z najważniejszych nagłówków w komunikacji HTTP. Informuje on serwer, którego wirtualnego hosta ma użyć, pod jaki adres chcemy wysłać zapytanie oraz określa, która aplikacja powinna przetwarzać przychodzące żądanie HTTP.

Nagłówek ten, wprowadzony w HTTP/1.1, to trzecia z najważniejszych informacji, której można użyć, oprócz adresu IP i numeru portu, w celu jednoznacznego zidentyfikowania serwera aplikacji lub domeny internetowej. Możesz sobie wyobrazić, że jest on pewnego rodzaju mechanizmem routingu na poziomie aplikacji — na jego podstawie serwery aplikacyjne decydują o dalszym sposobie przetwarzania żądania, a także umożliwiają obsługę wielu serwisów na jednym adresie IP.

W tym wpisie omówię, za pomocą jakich zmiennych możemy zarządzać tym nagłówkiem z poziomu serwera NGINX oraz w jaki sposób przekazać jego poprawną wartość do warstwy backendu.

Nagłówek Host a aplikacja #

Bardzo często, po otrzymaniu żądania, aplikacja wykorzystuje przesłany nagłówek Host w celu określenia sposobu obsługi żądania. Moim zdaniem poleganie na wartościach ustawionych w tym nagłówku jest złym pomysłem, ponieważ może umożliwić przekierowanie klienta do spreparowanych zasobów (poprzez wstrzyknięcie sfałszowanej wartości nagłówka do pamięci podręcznej), do miejsc w aplikacji, które nie powinny być dostępne z zewnątrz lub całkowicie do innego (niezamierzonego) serwera.

Jeśli serwer aplikacji nie jest właściwie skonfigurowany, napastnik będzie mógł wykorzystać technikę polegającą na sfałszowaniu wartości tego nagłówka, w celu uzyskania dostępu np. do funkcji administracyjnych serwera aplikacji lub zmylenia serwerów odpowiedzialnych za load-balancing, którym zdarza się podejmować decyzję o kierowaniu żądań na podstawie wartości tego nagłówka.

Jednak jeśli obsługa nagłówka Host jest wymagana po stronie aplikacji (prawie zawsze jest), jako administratorzy powinniśmy zagwarantować jego poprawną wartość (oczywiście jak bardzo jest to możliwe), aby upewnić się, że zachowanie hosta wirtualnego na dalszym serwerze działa tak, jak powinno.

Każdy klient musi dołączyć nagłówek Host do każdego żądania, a każdy odbiorca ma obowiązek rozpoznawania bezwzględnych adresów URL podawanych w pierwszym wierszu żądania. Zgodnie z RFC 7230 - Host [IETF], gdy serwer proxy (który jest szczególnie wrażliwy na fałszowanie tego nagłówka) odbierze żądanie w formie bezwzględnej (absolute-form [IETF]):

GET https://example.com/index.html HTTP/1.1

Zamiast „standardowej” postaci docelowego żądania, tj.:

GET /index.html HTTP/1.1
Host: example.com

Musi zignorować otrzymane pole nagłówka Host (jeśli istnieje, w tym przykładzie go nie ma) i zamiast tego zastąpić je informacją o hoście będącym celem żądania.

RFC 2616 - 5.2 The Resource Identified by a Request [IETF] dla HTTP/1.1 znosi to ograniczenie i nakazuje pomijać ten nagłówek. Oczywiście nie każdy z klientów stosuje się do tej zasady i powyższa interpretacja zależy tak naprawdę od implementacji, co może rodzić potencjalne problemy. Klient może podejmować istotne decyzje o żądaniu na podstawie nagłówka Host, który w przypadku formy bezwzględnej nie spełnia żadnej funkcji.

Serwer proxy, który przekazuje takie żądanie, musi wygenerować nową wartość nagłówka na podstawie otrzymanego celu żądania, a nie przekazywać odebraną wartość pola Host w żądaniu klienta. W takiej sytuacja serwer musi odpowiedzieć kodem 400 (Bad Request) na każdy komunikat żądania HTTP, który nie ma pola nagłówka Host, na każdy komunikat żądania zawierający więcej niż jedno pole tego nagłówka lub zawierający niepoprawną wartością (według mnie, także adres IP).

Oczywiście najważniejszą linią obrony jest odpowiednia implementacja mechanizmów weryfikujących po stronie aplikacji, np. wykorzystanie listy dozwolonych wartości nagłówka Host. Twoja aplikacja powinna być w pełni zgodna z RFC 7230 [IETF], aby uniknąć problemów spowodowanych niespójną interpretacją hosta w celu powiązania go z daną transakcją HTTP. Zgodnie z zaleceniami poprawnym rozwiązaniem jest traktowanie wielu nagłówków Host i białych znaków wokół nazw pól jako błędnych.

Nagłówek Host a NGINX #

NGINX udostępnia zmienne, które mogą przechowywać nagłówek Host dostarczony w żądaniu. Jedną z takich zmiennych jest zmienna $host, która zapisuje wartość tego nagłówka z małych liter i z pominięciem numeru portu (jeśli był obecny).

Wyjątkiem jest, gdy HTTP_HOST jest nieobecny lub jest pustą wartością. W takim przypadku $host jest równy wartości dyrektywy server_name, czyli serwera, który przetworzył żądanie.

Jednak spójrz na to wyjaśnienie:

An unchanged Host request header field can be passed with $http_host. However, if this field is not present in a client request header then nothing will be passed. In such a case it is better to use the $host variable - its value equals the server name in the Host request header field or the primary server name if this field is not present.

Wynika z tego, że jeśli ustawimy nagłówek Host w żądaniu na wartość Host: MASTER:8080, zmienna $host będzie przechowywać wartość master, podczas gdy wartość $http_host (kolejna zmienna) będzie równa MASTER:8080 (w taki sposób odzwierciedla ona cały nagłówek).

Zgodnie z tym $host to po prostu $http_host z pewnymi modyfikacjami (zostaje usunięty numeru portu oraz wykonana jest konwersja na małe litery) i wartością domyślną (server_name).

Zmienna $host to nazwa hosta z wiersza żądania lub nagłówka HTTP. Zmienna $server_name to nazwa bloku serwera, w którym przetwarzane jest żądanie.

Różnice wyjaśniono w dokumentacji NGINX:

$http_host ponadto jest lepszy niż konstrukcja $host:$server_port, ponieważ używa portu obecnego w adresie URL, w przeciwieństwie do $server_port, który używa portu, na którym nasłuchuje NGINX.

W związku z tym, aby poprawnie przekazać wartość nagłówka Host do aplikacji, należy wykonać to za pomocą poniższej konstrukcji:

proxy_set_header Host $host;

Takie ustawienie pozwala używać przeparsowanej nazwy hosta żądania lub nagłówka Host oraz gwarantuje, że wartość przekazana do kolejnej warstwy jest ustawiony tak, aby serwer nadrzędny mógł odwzorować żądanie na serwer wirtualny lub w inny sposób wykorzystać część hosta adresu URL wprowadzonego przez użytkownika.

Z drugiej strony, użycie zmiennej $host ma swoją własną podatność: musisz poradzić sobie z sytuacją, gdy pole nagłówka Host jest nieobecne, definiując domyślne bloki serwera, aby wychwycić takie żądania. Kluczową kwestią jest jednak to, że powyższa dyrektywa w ogóle nie zmieni tego zachowania, ponieważ wartość zawarta w zmiennej $host będzie równa wartości $http_host, gdy obecne będzie pole nagłówka w żądaniu HTTP.

Jeśli wymagane jest użycie oryginalnej nazwy wirtualnego hosta z pierwotnego żądania, możesz użyć zmiennej $http_host zamiast $host.

Aby temu zapobiec, należy wykorzystać wirtualne hosty typu catch-all posiadające ustawiony parametr default_server. Są to bloki, do których odwołuje się serwer NGINX, jeśli w żądaniu klienta pojawia się nierozpoznany lub niezdefiniowany nagłówek Host.

Również dobrym pomysłem jest podawanie dokładnej (niewieloznacznej) wartości w dyrektywie server_name, np.:

# Forma z dokładną nazwą:
server_name example.com api.example.com;

# Forma z nazwą wieloznaczną:
server_name *.example.com;

Alternatywy dla nagłówka Host #

Spójrz, co mówi na ten temat RFC 7540 - Request Pseudo-Header Fields [IETF]:

To ensure that the HTTP/1.1 request line can be reproduced accurately, this pseudo-header field MUST be omitted when translating from an HTTP/1.1 request that has a request target in origin or asterisk form. Clients that generate HTTP/2 requests directly SHOULD use the ":authority" pseudo-header field instead of the Host header field. An intermediary that converts an HTTP/2 request to HTTP/1.1 MUST create a Host header field if one is not present in a request by copying the value of the ":authority" pseudo-header field.

Oczywiście odnosi się to do protokołu HTTP/2, który dostarcza pseudonagłówek :authority będący alternatywą dla nagłówka Host w HTTP/1.1.