heise online

Wiadomości IT, artykuły i fora heise online

28 maja 2010, 15:54

CONFidence 2010: Kaminsky o (nie-)bezpiecznej sieci Web

Na odbywającej się w tym tygodniu w Krakowie konferencji CONFidence znany z odkrycia wymierzonych w DNS ataków Cache Poisoning badacz i konsultant ds. bezpieczeństwa Dan Kaminsky zaprezentował zagrożenia obecne w światowej sieci World Wide Web. Zazwyczaj prezentacje tego typu ograniczają się do mało odkrywczych fragmentów wyjaśniających słuchaczom, jak działają ataki typu Cross-Site Scripting (XSS) czy Cross-Site Request Forgery (CSRF). W tym przypadku jednak prelegent przedstawił precyzyjne diagnozy takiego stanu rzeczy, a także propozycje rozsądnych rozwiązań.

Sesje poza kontrolą


Dan Kaminsky opowiada o bolączkach zarządzania sesjami w aplikacjach WWW Powiększ
Pierwszym ważnym aspektem, o którym wspomniał, były sesyjne cookies – służą one utrzymywaniu trwającej w czasie relacji między użytkownikiem a przypisanymi mu zasobami serwisu, na przykład stroną, na której obecnie się znajduje, czy w przypadku sklepu online – liczbą zakupionych do tej pory przedmiotów. Gdyby nie istniały mechanizmy obsługi sesji, to korzystając choćby z bankowości elektronicznej, musielibyśmy podawać nazwę użytkownika i hasło na każdej z kolejnych podstron obsługujących dane zlecenie.

Jednak z mechanizmami sesyjnych ciasteczek związane są nierozerwalnie zagrożenia, których przyczyną jest brak precyzyjnego i przejrzystego sposobu odróżniania działań podejmowanych lokalnie na zlecenie użytkownika (np. pobranie dokumentu przez kliknięcie odnośnika) od działań zautomatyzowanych (np. odebranie przekierowania lub odwiedzenie odnośnika spowodowane wywołaniem skryptu). W związku z tym istnieje wiele scenariuszy, w których potencjalny napastnik jest w stanie nakłonić przeglądarkę do wykonania nieautoryzowanych działań poświadczonych identyfikatorem sesji zapisanym w cookie. Podsumowując: przyczyną problemu z zagwarantowaniem wiarygodnej i bezpiecznej sesji pracy z danym serwisem WWW są niepożądane ciasteczka strony trzeciej (third party cookies).

Według Kaminskiego odpowiedzialne są za to przede wszystkim różnice w sposobach obsługi pewnych zdarzeń w browserach. Jako przykład niekompletnego obejścia problemu bezpiecznej sesji podał on stosowanie w niektórych serwisach nagłówka Referer, który ustawiany jest zależnie od modelu przeglądarki. Wspomniany nagłówek zawiera zwykle adres URL strony, która była wyświetlana użytkownikowi, zanim wywołano bieżącą – niestety, na nagłówku tym nie można w stu procentach polegać. Załadować kolejną stronę WWW można bowiem na wiele sposobów: wpisując jej adres ręcznie, klikając odnośnik, zmieniając document.location.href, podsuwając przekierowanie 302, umieszczając w kodzie znacznik META z atrybutem http-equiv ustawionym na refresh czy wywołując metodę Window.open(). Z powodu tego typu niezgodności programiście trudno jest znaleźć jakiś standardowy wskaźnik, który miałby pomagać w weryfikacji tego, czy żądania browsera wynikają z działań użytkownika, czy może pochodzą z podsuniętego przekierowania lub szkodliwego skryptu umieszczonego w odległych zakątkach Sieci.

Pewnym rozwiązaniem problemu byłoby odgórne ograniczenie przyjmowania cookies do domeny wpisanej w pasku – jednak gdyby wprowadzić ten pomysł w życie, większość współczesnych serwisów WWW przestałaby poprawnie działać. Przykładem mogą być tu profilowane reklamy emitowane przez zewnętrzne strony WWW albo łączność między różnymi witrynami Web 2.0 służąca wymianie danych z użyciem klienta.

Oczywiście funkcja ograniczania widoczności "ciasteczkowych" zmiennych już istnieje, ale współczesne mechanizmy skryptowe pozwalają zaburzyć jej przewidywalne działanie. Aby być pewnym wprowadzonych ograniczeń, należałoby głęboko ingerować w uformowane przez lata standardy programowania WWW. W związku z tym trudno byłoby oczekiwać szybkiej poprawy bezpieczeństwa w omawianym obszarze zastosowań, ponieważ istnieje potrzeba zapewnienia tzw. zgodności wstecznej – należy ją zachować, aby wszystkie wcześniej tworzone strony WWW nie miały problemów z poprawną pracą skryptów. Większe serwisy pomagają sobie w ochronie sesji, wprowadzając umieszczane najczęściej w ukrytych polach formularzy tokeny, których wartość zależy od parametrów pracy klienta i poprzednio odwiedzonego zasobu. Problem polega na tym, że w niektórych przypadkach szkodliwy skrypt może również zmanipulować token, a nawet jeśli tak nie jest, to podejście to wymaga w zasadzie zaprojektowania od podstaw mechanizmu obsługi sesji i nie każdy właściciel serwisu internetowego ma możliwości, żeby go zaimplementować.

Dan Kaminsky lubi radykalne metody radzenia sobie z kłopotami, dlatego jego zdaniem rozszerzanie już istniejących sposobów zarządzania sesją o nowe cechy, takie jak dodawanie nowego nagłówka HTTP, przypominałoby łatanie dziurawej beczki sitem. Uważa natomiast, że tak naprawdę potrzebne jest specjalne sesyjne supercookie, związane nie z przeglądarką, ale z jej konkretnym oknem, do którego wczytywane są dokumenty pochodzące z danego serwisu. Zaproponował przy okazji kilka funkcji JavaScriptu, które pozwalałyby serwisom ustawiać, odczytywać, zapisywać i niszczyć tak stworzony obiekt. Zadaniem przeglądarki byłoby niedopuszczanie, aby skrypty działające w kontekście obsługi jakiegokolwiek innego okna (czy znanej z wielu browserów tzw. zakładki) miały dostęp do tego nadzwyczajnego obiektu. Kaminsky zwrócił też uwagę na konieczność zagwarantowania bezpiecznych operacji inicjowania tego typu sesji, tzn. nie pozwalania innemu skryptowi na działanie w kontekście danego okna (zakładki) aż do zakończenia procesu ustanawiania supercookie. Dzięki temu żaden potencjalnie niebezpieczny skrypt załadowany z zewnątrz nie mógłby odczytać tajnego identyfikatora sesji przez przechwycenie wywołań funkcji.

Ciasne stringi

heise Security: W jaki sposób możemy zapewnić, że zakodowana (np. z użyciem Base64) porcja danych wejściowych zostanie zdekodowana we właściwym miejscu?

Dan Kaminsky: Ostatnimi czasy okazało się, że aplikacje webowe napisane w tak zwanych bezpiecznych językach (takich jak Java, C#) nie były tak naprawdę bezpieczniejsze niż aplikacje napisane w językach uznawanych za mało bezpieczne (np. PHP). Nie ma znaczących różnic między lukami znajdowanymi w tych programach. Jednak na tworzenie bezpiecznych języków poświęca się więcej czasu, właśnie dlatego, że z założenia mają być bezpieczniejsze. Dlaczego więc nie osiągamy tego celu? Moim zdaniem większość kłopotów z bezpieczeństwem WWW – nie wszystkie, lecz większość – występuje na styku języków programowania: gdy C# komunikuje się z SQL-em, gdy Java "rozmawia" z XML-em. W tym miejscu dochodzi do pomieszania w typach danych, ponieważ te graniczne przestrzenie są obsługiwane z użyciem łańcuchów tekstowych.

Kaminsky jest pragmatykiem i zdaje sobie sprawę, że świat aplikacji WWW rządzi się własnymi prawami, a najpopularniejszymi technologiami wcale nie zostają te najbezpieczniejsze czy najbardziej wydajne. Mimo istnienia całej gamy nowoczesnych, obiektowych, dynamicznie typizowanych języków programowania sieciowego, pierwsze skrzypce w Internecie grają aplikacje stworzone na przykład w używanym proceduralnie PHP czy korzystające z bazy danych w sposób bezpośredni, z pominięciem warstw abstrakcji gwarantujących choćby bezpieczną konwersję danych wejściowych.

Ataki typu SQL Injection są zmorą wielu administratorów baz danych i programistów. Problemem są w tym przypadku znaki tekstowe specjalnego znaczenia, które umiejętnie umiejscowione w żądaniu, są w stanie sprawić, że silnik bazy danych wykona podsuniętą przez intruza operację. Główną przyczyną według Kaminskiego jest nie tyle niedbałość programistów, zapominających o usuwaniu znaków mających dla silnika bazy danych specjalne znaczenie, ale nieumiejętnie zaprojektowane interfejsy działające na pograniczu różnych języków programowania i innych technologii przetwarzania danych.

Sam fakt wstawiania przez programistów zmiennych zawierających dane wejściowe (np. pochodzące z formularza na stronie WWW) bezpośrednio do instrukcji generujących zapytania jest zdaniem prelegenta naturalną tendencją, która pozwala projektantowi orientować się w kodzie i szybciej tworzyć interfejsy przesyłania danych. Podobny problem występuje też w przypadku przetwarzania dokumentów XML, gdzie odpowiednio umieszczony znacznik zamykający może zakłócić proces przetwarzania i spowodować usterkę danych wyjściowych lub wstrzymanie pracy danego wątku usługi. Tego typu usterką jest również znaleziony ponad rok temu błąd w obsłudze certyfikatów X.509 – wstawiając w odpowiednie miejsce znak o kodzie 0, można było zlecić cyfrowe podpisanie certyfikatu wystawionego na fałszywą nazwę.

hS: Wyobraź sobie dwa modele zastosowania Twojego pomysłu z użyciem Base64. Pierwszy zakłada przekazanie zakodowanej wersji danych aż do ostatniego ogniwa w łańcuchu systemów wymieniających informacje, a drugi wykonuje operację kodowania i dekodowania za każdym razem, gdy te przechodzą między interfejsami. Który sposób jest Twoim zdaniem lepszy?

DK: Preferowanym przeze mnie sposobem implementowania tego rodzaju kodowania jest wykonywanie go tuż przed przekazaniem danych do następnego języka. Gdy mamy duży BLOB i jakiś jego fragment pochodzący od "złoczyńcy", to chcemy, by jego bezpieczne zakodowanie odbyło się w momencie przekazywania obsługi kolejnemu językowi, w którym będzie on zdekodowany. Na prezentowanych przeze mnie slajdach pokazywałem przykłady wywołań funkcji opakowujących, umieszczonych w zapytaniach SQL występujących w kodzie, a nazwanych bd() od Base64 decode. Korzysta się z nich tylko podczas przekazania danych z formularza, bo właśnie tam mamy najczęściej do czynienia z naruszeniami bezpieczeństwa.

Pierwotną przyczyną opisanych kłopotów jest według Kaminskiego to, że od dziesięcioleci do wymiany danych między różnymi systemami ich przetwarzania używa się łańcuchów znaków (strings). Jeśli do relacyjnej bazy danych ma trafić wprowadzona przez użytkownika informacja, na przykład o jego wieku, zweryfikowana wcześniej przez aplikację działającą w C++, a wprowadzona przez interfejs napisany w PHP, to mamy do czynienia przynajmniej z sześcioma konwersjami. Najpierw przeglądarka dokona konwersji zgodnie z regułami zestawu znaków zaproponowanego przez serwer (np. UTF-8), a następnie aplikacja PHP może na podstawie ustawień języka spróbować konwersji znaków odpowiedzialnych za oddzielanie części całkowitej od dziesiętnej (w przypadku wprowadzenia przecinka lub kropki). Kolejnym krokiem będzie zapamiętanie wartości liczbowej wprowadzonego łańcucha, aby za chwilę przekształcić ją znowu w tekst – tym razem umieszczony wewnątrz znaczników XML, które "zrozumie" program sprawdzający napisany w C++. Ten ostatni połączy się z bazą SQL, lecz zanim to zrobi, znów zmieni wartość liczbową w tekst i umieści w zapytaniu skierowanym do silnika bazy. Zauważmy, że każdy z tych komponentów może być wrażliwy na inne sekwencje znaków mające specjalne znaczenie. Dla interpretera PHP ważny będzie przecinek lub kropka, decydujące o konwersji do zmiennej numerycznej. Z kolei dla posługującego się XML-em programu w C++ istotne będzie, czy aby nie zawarto w przekazywanym łańcuchu znaku otwarcia lub zamknięcia znacznika XML. W końcu, gdy dane dotrą do bazy danych, ważną z punktu widzenia bezpieczeństwa sekwencją sterującą będą apostrofy. Problem: który komponent powinien być odpowiedzialny za eliminowanie niebezpiecznych symboli z przejściowych łańcuchów znaków i w jaki sposób ma się dowiedzieć o niebezpiecznych symbolach w kontekście poszczególnych podsystemów przetwarzania?

Dopóki jedynym obowiązującym zestawem znaków był ograniczony do ponad dwustu pozycji alfabet ASCII, można było sobie z tym problemem poradzić, stosując pewne filtry eliminujące wszelkie znaki poza alfanumerycznymi. Nawet wtedy zdarzały się (i zdarzają) pomyłki, spowodowane zazwyczaj użyciem nie do końca poprawnych wyrażeń regularnych. Jednak prawdziwy problem pojawił się wraz z nadejściem nowych sposobów bajtowego kodowania znaków, obecnych na przykład w przestrzeni numeracyjnej zestawu Unicode. W tym przypadku technologia filtrowania komplikuje się, bo zamiast jednego odpowiednika znaku o kodzie zero w zestawie Unicode mamy ich kilka; podobnie ze spacją i innymi znakami niebędącymi alfanumerykami.

Z powyższych względów Kaminsky proponuje użyć starego i sprawdzonego sposobu transportowego kodowania danych między różnymi systemami przetwarzania znanego jako Base64. W związku z tym zamiast filtrowania wyłączającego (usuwanie niepożądanych sekwencji) niejako automatycznie zastosowane będzie filtrowanie dopuszczające (pozwalanie tylko na określone znaki).

hS: Czy w praktyce nie lepiej użyć innego kodowania niż Base64, ze względu np. na wielkość danych?

DK: Moja prelekcja dotyczyła tego, w jaki sposób łatwiej pisać bezpieczny kod. To dlatego, że wiele metod określających, jak to robić, jest bardzo trudnych do zastosowania przez programistów. Będą ich używali, jeśli ich o to poprosimy, wybłagamy to lub do tego zmusimy. Jednak celem mojego wystąpienia było pokazanie, że możemy dać ludziom lepsze, mniej psujące się narzędzia. Wykorzystałem kodowanie Base64, ponieważ jest łatwe i niełamliwe. Przeprowadziłem też odpowiednie testy wydajnościowe i nie było większej różnicy między przekazywaniem do bazy danych zakodowanych z użyciem Base64 a łańcuchów tekstowych poddawanych filtrowaniu. Poza tym mamy pewność, że ten sposób kodowania działa i że już jest wykorzystywany. Podejście to jest nie tylko bezpieczne, ale też można szybko je zweryfikować.

Warto zauważyć, że gdy na interfejsach wymiany danych między różnymi językami programowania zastosujemy zdefiniowane wprost kodowanie transportowe, a nie tylko konwersję do łańcucha znaków (o często niedookreślonym kodowaniu bajtowym), to pokusić możemy się również o wprowadzenie definiowalnych terminatorów, czyli sekwencji wyznaczających początek i koniec przenoszonych danych. Użycie takiej metody, podobnie jak w standardzie MIME, zapewnia bezpieczne przesyłanie danych, odporne na wstrzykiwanie symboli specjalnych i manipulowanie znacznikami początku i końca. Potencjalny napastnik, który chciałby umieścić fałszywy znacznik kończący fragment zakodowanych danych musiałby się domyślić długiej, losowej sekwencji sygnalizującej koniec pewnej sekcji danych.

Problemem wydaje się tutaj pewien narzut czasowy związany z kodowaniem i dekodowaniem informacji, a także zwiększanie rozmiaru danych o około 30 procent – ta ostatnia właściwość wynika z zawężenia liczby dopuszczalnych znaków używanych do ich transportowania, tzn. z ograniczonego alfabetu. Jednak Kaminsky zwraca uwagę, że w tej chwili również korzysta się z nawet mniej oszczędnego kodowania, na przykład używając HTML-owych encji czy funkcji w rodzaju urlencode(). Jeśli chodzi o czas kodowania i dekodowania, to często nie jest on problemem, jeśli porównać go z czasem kompilacji i wykonywania wyrażeń regularnych używanych do usuwania niebezpiecznych znaków.

W rozmowie z heise Security Kaminsky zapewnił, że za kilka tygodni ukończy prace nad odpowiednimi funkcjami UDF, które działając w bazach danych, będą w stanie dekodować dane przekształcone do postaci Base64. W tej chwili pracuje nad modyfikacjami w bazach MySQL, PostreSQL, Oracle i MS SQL.

(pwi)

  • Podziel się
  • Wykop.pl
  • StumbleUpon
  • del.icio.us
  • OSnews.pl