niedziela, 4 grudnia 2022

API ASP.NET Core - Walidacja danych wejściowych

 Dane wejściowe w API możemy sprawdzać na dwa sposoby:

  1. Korzystając z atrybutów walidacji na modelu
  2. Tworząc własną logikę walidacji w klasie Controllera

1. Stworzenie walidacji jako atrybutu modelu

Jako przykład możemy stworzyć atrybut, który sprawdzi, że wszystkie znaki ciągu teksu są wielkimi literami:

using System.ComponentModel.DataAnnotations;
using System.Globalization;
using static System.String;

namespace BookStoreApi;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class UpperCaseAttribute : ValidationAttribute
{
    public UpperCaseAttribute() : base("All characters for {0} field must be upper case")
    {
    }

    public override bool IsValid(object? value)
    {
        return (value is string str) && str.All(char.IsUpper);
    }

    public override string FormatErrorMessage(string name)
    {
        return Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
    }
}


Ważniejsze punkty implementacji
  • klasa którą implementuje powinna dziedziczyć po ValidationAttribute
  • W konstruktorze możemy (anie nie musimy) przekazać error message, który zostanie wyświetlony użytkownikowi jeżeli walidacja się nie powiedzie. Możemy także zostawić miejsce na nazwę pola, która zostanie wstrzyknięta do wiadomości błędu - {0}
  • Metoda FormatErrorMessage zawiera parametr name, który reprezentuje sprawdzane pole
Atrybut dodajemy na pole naszego obiektu DTO i możemy sprawdzić jego działanie:

public class BookDto
{
    public int Id { get; set; }

    [Required, MaxLength(250), MinLength(3)]
    public string Name { get; set; }

    [Required, MaxLength(250), MinLength(3), UpperCase]
    public string Author { get; set; }

    public int PublishYear { get; set; }
}



2. Własna logika w klasie Controllera

Logikę walidacji można także zawrzeć bezpośrednio w klasie Controllera:

    public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookDto bookDto)
    {
        if (bookDto == null)
        {
            return BadRequest(bookDto);
        }

        if (bookDto.Id > 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }

        if (!bookDto.Author.All(char.IsUpper))
        {
            ModelState.AddModelError("AuthorValidation", "All characters for Author field must be upper case ccc");
            return BadRequest(ModelState);
        }
        //... code....
     }
Po uruchomieniu aplikacji i przetestowaniu, analogicznie jak poprzednio otrzymamy błąd w przypadku gdy pole Author będzie zawierało jakiekolwiek małe litery. 

Obydwa sposoby walidacji są jak najbardziej poprawne. Drugi z jednej strony może wydawać się mniej skomplikowany (mniej kodu), jednak stworzenie atrybutu pozwala na jego łatwe re-używanie.

poniedziałek, 7 września 2020

OWASP 2017 - 10 najczęstszych zagrożeń aplikacji internetowych (web) - A3 - A10

Kolejnymi zagrożeniami na liscie OWASP 2017 są:

A3. Sensitive Data Exposure

Powszechne zagrożenie dla aplikacji internetowych - wyciek danych. Dane te to np. numery PESEL, karty kredytowe etc. Najczęstszą przyczyną wycieków tych danych są słabe zabezpieczenia bazy danych, brak szyfrowania danych czy też użycie słabych algorytmów szyfrowania. 
W jaki sposób zapobiec wyciekowi poufnych danych?
  • ogranicz ilość przechowywanych danych. Być może nie potrzebujesz ich w ogóle w aplikacji? Jeżeli jednak są wymagane do poprawnego działania systemu sprawdź regulacje prawne 
  • szyfruj dane oraz użyj bezpiecznych protokołów (TLS)
  • szyfruj hasła używając silnych algorytmów (Argon2, PBKDF2). Unikaj stosowania MD5, SHA1, SHA256. Bądź na bieżąco z obecnymi standardami - algorytmy zmieniają się, wymyślane są nowe
  • kiedy przeglądarka wyświetla poufne dane dodaj dyrektywy, które zapobiegną użycia cache dla poufnych informacji

A4. XML External Entities (XXE)

External Entities to elementy pozwalające budować XML w sposób dynamiczny. Podczas przetwarzania XMLa umożliwiają wstrzykiwanie definicji elementów z zewnętrznych źródeł.
Kiedy aplikacja jest narażona na ten typ ataku?
  • aplikacja jako dane wejściowe przyjmuje bezpośrednio XML
  • użycie SOAP < 1.2
  • SOAPowy serwis używa DTD (document type definitions)

A5. Broken access control

Atakujący uzyskuje dostęp do funkcjonalności, do których normalnie nie miałby dostępu. Może się to odbyć np. poprzez modyfikację adresu url strony i uzyskanie dostępu do panelu administracyjnego, bądź też funkcji która jest normalnie widoczna tylko dla moderatora/administratora. 
W jaki sposób bronić się przed tym typem ataku?
  • warstwa autoryzacji powinna być zaimplementowana w jednym komponencie i reużywana przez wszystkie inne składowe systemu
  • domyślnie zawsze zabraniamy dostępu do danego zasobu
  • logujemy błędne próby logowania i analizujmy incydenty związane z wieloma próbami zalogowania się przy użyciu błędnych uwierzytelnień 
  • sesja użytkownika powinna być prawidłowo kończona po jego wylogowaniu z systemu

A6. Security misconfiguration

Ten typ zagrożenia wynika z stosowania domyślnych kont administratora, pozostawianiu niezabezpieczonych folderów na serwerze itp. Atakujący może w ten sposób dowiedzieć się więcej o systemie i poznać jego słabe punkty. Zagrożenia tego typu możemy spodziewać się na każdym poziomie: sieć, system, framework, aplikacja. 
W jaki sposób zapobiegać tego typu atakom?
  • automatyzacja manualnej konfiguracji (zwłaszcza powtarzalnej konfiguracji)
  • środowiska (testowe, dweloperskie i produkcyjne) powinny mieć taka samą konfigurację
  • nie instalujemy na serwerach zbędnego oprogramowania
  • cyklicznie aktualizujemy system serwera i oprogramowanie na nim zainstalowane

A7. Cross-Site Scripting (XSS)

Atak ten polega na umieszczeniu niebezpiecznego kodu w treści strony. Nieświadomy użytkownik wchodząc na stronę uruchamia niepożądany kod infekując swój komputer. XSS dzielimy na trzy typy: Reflected XSS, Stored XSS, DOM based XSS. 
Jak się zabezpieczyć przed tego typu atakiem?
  • używajmy gotowych narzędzi w frameworkach
  • poznajmy słabe strony frameworka, który używamy
  • sprawdzamy dane pochodzące od użytkownika
  • unikamy pracy z czystym HTMLem/JSptem

A8. Insecure deserialization

Zagrożenie to polega na wprowadzeniu do zserializowanej wiadomości nieuprawnionej treści. Treść ta może wpłynąć na sposób przetwarzania kodu, wywołać funkcje, których w normalnym procesowaniu byśmy nie wywołali. 
Najlepszym sposobem obrony przed tym typem ataku jest nie przyjmowanie zserialozowanej treści z nieznanych źródeł. Innym sposobem walki jest deserializacja obiektów w odizolowanym środowisku z bardzo niskimi uprawnieniami. Nawet jeżeli do naszego systemu przedostanie się niechciany pakiet danych nie będzie on miał uprawnień do wykonania niebezpiecznych operacji. Monitorowania i logowanie błędów deserializacji - bardzo często da nam to informacje z jakiego źródła pochodzą podejrzane wiadomości. 

A9. Using components with known vulnerabilities

Tu sprawa wydaje się dosyć prosta. Oprogramowanie zmienia się. Frameworki zmieniaja się. Wykrywane są nowe błędu i publikowane poprawki do nich. Uruchamiając aplikację w środowisku produkcyjnym musimy mieć świadomość, że po pewnym czasie będziemy musieli uaktualnić działające na nim oprogramowanie, framworki, bazy danych itp. 
Dobrym sposobem zabezpieczenia jest także ograniczenie ilości oprogramowania uruchomionego na środowisku produkcyjnym - instalujemy tylko to co jest niezbędne. Warto także prowadzić i przestrzegać harmonogramu uaktualniania oprogramowania co pozwoli być na bieżąco ze wszystkimi aktualizacjami. Jeżeli instalujemy oprogramowanie - używajmy oficjalnych kanałów. Warto także monitorować serwisy poświęcone tematyce bezpieczeństwa np. https://www.cvedetails.com/version-search.php. 

A10. Insufficient logging and monitoring
To przykład w jaki sposób pominięcie dobrego monitorowania i logowania może zagrozić naszemu systemowi. Atakujący wręcz liczy, że przez źle zaimplementowany monitoring zyska czas potrzebny do przeprowadzenia skutecznego ataku. Można też pójść w drugą stronę - logować zbyt wiele co prowadzi do kompletnego chaosu i uniemożliwia analizę logów. 
Dobre logowanie przechwyci z pewnością wszelkie próby logowania do systemu z błędnymi poświadczeniami (dane te następnie możemy wykorzystać i zablokować potencjalnego atakującego). Logowanie powinno być spójne i przechowywane w centralnym miejscu umożliwiającym analizę danych. Możemy na przykład skorzystać z ELK bądź Splunk w celu magazynowania i przetwarzania logów. Oprócz logowania musimy zadbać też o odpowiednie powiadamianie. 

wtorek, 25 sierpnia 2020

OWASP 2017 - 10 najczęstszych zagrożeń aplikacji internetowych (web) - A2. Broken Authentication

Drugim najczęściej występującym problemem bezpieczeństwa aplikacji internetowych jest błędnie zaimplementowana autoryzacja użytkowników. 

Kiedy aplikacja może być podatna na ten rodzaj ataku?

  • aplikacja zezwala na proste hasła, które są dobrze znane (np. admin123)
  • domyślne hasło dla administratora nie zostało zmienione podczas implementacji rozwiązania u klienta
  • brak zabezpieczenia przed atakami typu brute-force
  • brak możliwości użycia logowania wieloetapowego (np. za pomocą hasła i SMSa)
  • odzyskiwanie hasła za pomocą metod pamięciowych (np. pytania typu jak się nazywało Twoje pierwsze zwierze)
  • użycie słabych algorytmów szyfrowania haseł w bazie, przechowywanie ich w formie tekstu
  • Brak zarządzania sesjami użytkowników. Sesja nie wygasa co powoduje możliwość jej przejęcia przez atakującego.

W jaki sposób można się zabezpieczyć przed tym typem zagrożenia?
  • zmień domyślne hasła przed wgraniem systemu na produkcję
  • użyj algorytmów sprawdzających siłę i składnię hasła które chce użyć użytkownik, wymuś stosowanie silnych haseł
  • wprowadź możliwą ilość błędnych logowań do systemu, bądź też wprowadź czasowy odstęp miedzy ponownymi próbami logowania. Dodaj alerty w systemie monitorującym w przypadku gdy ktoś próbuje w kółko logować się używając błędnego hasła
  • zaimplementuj wieloetapową autoryzację
  • logika odzyskiwania hasła, podobnie jak autoryzacja może przebiegać wieloetapowo
  • zarządzanie sesjami użytkowników powinno odbywać się tylko na serwerze aplikacji. Nie twórz własnych rozwiązań do generowania sesji - użyj gotowych, dopracowanych rozwiązań.


poniedziałek, 24 sierpnia 2020

OWASP 2017 - 10 najczęstszych zagrożeń aplikacji internetowych (web) - A1. Injection

OWASP (Open Web Application Security Project) - otwarta społeczność tworząca artykuły, metodologie, narzędzia związane z bezpieczeństwem aplikacji internetowych. W ostatnich dniach miałem okazję wziąć udział w szkoleniu z tego zagadnienia i myślę, że warto się podzielić tą wiedzą.

Szkolenie dotyczyło 10 największych ryzyk bezpieczeństwa w zaktualizowanej wersji (2017). Jak można się domyślić co jakiś czas lista jest aktualizowana. Wynika to ze względu i na postęp technologiczny (frameworki rozwiązują część problemów) jak i nowe metodyki ataków na aplikacje internetowe. 

Jak przedstawia się lista OWASP 2017?

A1. Injection

A2. Broken authentication

A3. Sensitive data exposure

A4. XML External Entities (XXE)

A5. Broken access control

A6. Security disconfiguration

A7. Cross-site scripting (XSS)

A8. Insecure deserialization

A9. Using components with known vulnerabilities 

A10. Insufficient logging and monitoring


Listę rozpoczyna Injection który króluje na niej od 2010 roku. Najłatwiej można go opisać jako możliwość przesłania nieprawidłowych danych do zapytań. Konsekwencje tego zagrożenia:

  • dostęp do danych, których nie powinien widzieć atakujący
  • uruchamianie komend systemowych
Gdzie możemy spotkać się z tym typem ataków? Zapewne każdy kojarzy możliwość wstrzykiwania niepożądanego kodu do zapytań SQL. Oprócz SQL atakujący może eksplorować także:
  • LDAP
  • SMTP
  • OS Commands
  • Enviromental Variables
  • NoSQL
W jaki sposób chronić się przed tym atakiem?
  • użycie frameworków (np. ORM przy dostępie do danych)
  • oddzielenie parametrów zapytań od samego zapytania (użycie parametrów zamiast łączenia zapytania za pomocą stringów)
  • jeżeli jest to możliwe, zabronienie używania użytkownikowi systemu specjalnych znaków w zapytaniach
  • przeglądanie kodu w poszukiwaniu słabych punktów
  • użycie statycznej/dynamicznej analizy kodu na serwerze CI (np. Veracode scan)

W kolejnych postach omówię kolejne typy zagrożeń

niedziela, 23 sierpnia 2020

ArraySegment, Span - dwa typy które warto poznać

 W ostatnim wpisie mowa była o nowościach C# 8.0 - Index oraz Range. Jak wtedy wspominałem typ Range tworzy nową tablicę i kopiuje do niej wartości z zadanego przedziału. Ma to swoje implikacje w przypadku aplikacji nastawionych na dużą wydajność. Czy istnieje więc sposób, który umożliwiłby dostęp do zadanego przedziału bez potrzeby kopiowania oryginalnego źródła? Oczywiście, że tak. Przychodzą nam z pomocą dwie klasy ArraySegment<T> oraz Span<T>. Klasy te pozwalają uniknąć dodatkowego alokowania pamięci i kopiowania danych. Zobaczmy na przykład:

        static void Main(string[] args)
        {
            int[] tab = { 1, 2, 3, 4, 5, 6 };
            ArraySegment<int> range = tab[2..4];
            PrintContent(range);
        }

        private static void PrintContent(ArraySegment<int> arraySegment)
        {
            Console.WriteLine(string.Join(", ", arraySegment));
            Console.WriteLine(new string('-', 40));
        }

Powyższy kod wyświetli trzeci i czwarty element tablicy tab. Klasa ArraySegment jest ograniczona tylko do tablic. Należy także pamiętać, że klasa ta nie blokuje nas przed modyfikacją oryginalnej tablicy. Na nasze szczęście istnieje drugi typ, który pozwala pracować nad sub - sekwencjami innych typów (tablic, ciągów znaków, pamięć, pamięć zadeklarowana przez inne biblioteki itp) - Span<T> oraz ReadOnlySpan<T>. Zobaczmy na przykład użycia:

            int[] numbers = { 1, 2, 3, 4, 5, 6 };
            Span<int> range = numbers[2..4];
            ReadOnlySpan<char> textSpan = "someString"[2..4];


sobota, 22 sierpnia 2020

Nowe operatory indeksu i zakresu dla tablic w C# 8.0

C# 8.0 wniósł dwie nowości dla tablic jak i wszystkiego co implementuje interfejs IList<T>. Te dwie nowości to Index i Range:


Pierwszą z nowości jest Index - jak sama nazwa wskazuje jest to pozycja elementu w tablicy. Index to typ wartościowy - struktura readonly. Tworzyć obiekt tego typu możemy jak dla każdej innej struktury:


            int[] tab = { 1, 2, 3, 4, 5, 6 };

            var index0 = new Index(1, fromEnd: true);
            Console.WriteLine(tab[index0]);


Dodatkowy parametr (opcjonalny) fromEnd pozwala zdefiniować czy szukamy elementu o indeksie 1 od początku kolekcji czy od końca. W tym przypadku otrzymamy 1 wartość od  końca czyli 6. Index nie musi być "ręcznie" tworzony w kodzie - możemy posłużyć się operatorem ^ i tak aby pobrać ostatni element wystarczy zapis:


            int[] tab = { 1, 2, 3, 4, 5, 6 };

            Console.WriteLine(tab[^1]);


Ktoś może zapytać: "zaraz, skoro tablice są numerowane od 0, to dlaczego ostatni element pobieramy za pomocą ^1 a nie ^0"?

Sprawa jest dosyć prosta patrząc na poniższy rysunek, przedstawiający 4 elementową tablicę:


Kiedy chcemy pobrać element kolekcji (np. tablicy) poprzez indeks - żądamy pobrania elementu który zaczyna się na danej pozycji. Czyli dla przykładu jeżeli chcemy pobrać element na pozycji 0 to znaczy że chcemy odczytać wartość zawartą między indeksami 0 i 1. Dlatego własnie, patrząc na rysunek jeżeli chcielibyśmy pobrać wartość elementu ^0 oznaczałoby to chęć pobrania elementu tablicy który jest za ostatnim elementem tablicy, czyli chcemy odczytać pamięć która już nie należy do bloku tablicy. Próba taka zwróci oczywiście błąd. W drugą stronę działa to tak samo - jeżeli chcielibyśmy odczytać element na pozycji 4. Ponieważ tablica ma tylko 4 elementu, nie ma ona wartości dla indeksu między 4 i 5. 


Drugą nowością jest operator zakresu. Zakres to nic innego jak dwa indeksy: początek i koniec. Najłatwiej będzie zaprezentować ten operator na przykładach:


        static void Main(string[] args)
        {
            int[] tab = { 1, 2, 3, 4, 5, 6 };
            var range1 = new Range(2, 3);
            var range2 = 2..3;
            var range3 = 1..^2;
            var range4 = Range.All;
            var range5 = Range.EndAt(2);
            var range6 = 0..^1;
            var range7 = ..;
            var range8 = 2..;
            var range9 = ..2;
            PrintContent(tab, range1, nameof(range1));
            PrintContent(tab, range2, nameof(range2));
            PrintContent(tab, range3, nameof(range3));
            PrintContent(tab, range4, nameof(range4));
            PrintContent(tab, range5, nameof(range5));
            PrintContent(tab, range6, nameof(range6));
            PrintContent(tab, range7, nameof(range7));
            PrintContent(tab, range8, nameof(range8));
            PrintContent(tab, range9, nameof(range9));
        }

        private static void PrintContent(int[] tab, Range range, string rangeName)
        {
            Console.WriteLine(rangeName);
            Console.WriteLine(string.Join(", ", tab[range]));
            Console.WriteLine(new string('-', 40));
        }

Po zapoznaniu się z częścią poświęconą indeksom bardzo łatwo zrozumieć operator zakresu. Warto zapamiętać domyślne wartości:
  • jeżeli nie podamy lewego zakresu (startowego) będzie to zawsze 0
  • jeżeli nie podamy prawego zakresu (końcowego) będzie to zawsze -1
  • jeżeli stworzymy pusty obiekt zakresu (new Range()) zostanie stworzony zakres 0..0, czyli nie pobierzemy żadnego elementu. Jest to o tyle istotne, że domyślnie dla zakresu ".." dostajemy całą tablicę.
W jaki sposób działa Range? Działanie jest proste - zostaje stworzona nowa tablica i przekopiowane do niej wartości z zadanego zakresu. Zakres działa także dla typu string. Stosując go na obiektach łańcuchowych otrzymujemy sub-string. 

piątek, 21 sierpnia 2020

Modyfikowanie pola klasy, które jest strukturą

W jednym z ostatnich postów pisałem o użyciu słowa kluczowego ref w kontekście metod, pól i właściwości. Dzisiaj coś praktycznego, aczkolwiek raczej mało spotykanego zachowania. Zobaczmy na poniższy przykład:

    class Program
    {
        static void Main(string[] args)
        {
            var rectangle = new Rectangle(new Point3D(1, 2, 3));
            rectangle.A.X = 10; //Error CS1612  Cannot modify the return value of 'Rectangle.A' because it is not a variable
        }
    }

    public class Rectangle
    {
        public Point3D A { get; }

        public Rectangle(Point3D a)
        {
            A = a;
        }
    }

    public struct Point3D
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }

        public Point3D(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public double DoSomeCalculation()
        {
            return X * Y * Z;
        }
    }

Klasa Rectangle ma pole typu struct Point3D. Pola struktury Point3D pozwalają na zmianę ich wartości (posiadają settery). W metodzie Main tworzymy obiekt klasy Rectangle i chcemy zmodyfikować wartość właściwości X struktury. Niestety kompilator na to nie pozwala. Dlaczego?
Właściwości to nic innego niż pole plus metody get/set - dlatego właśnie kompilator nie pozwala na przypisanie wartości do pola. Wywołując metodę tak na prawdę tworzymy kopię której wartość została by zmieniona. Kompilator nie pozwala nam zrobić takiego błędu.

Pozostaje pytanie w jaki sposób więc umożliwić zmianę wartości tego pola? Możemy zastosować słowo kluczowe ref:

    class Program
    {
        static void Main(string[] args)
        {
            var rectangle = new Rectangle(new Point3D(1, 2, 3));
            rectangle.A.X = 10; //Error CS1612  Cannot modify the return value of 'Rectangle.A' because it is not a variable
            Console.WriteLine(rectangle.A.X);
        }
    }

    public class Rectangle
    {
        private Point3D _a;
        public ref Point3D A => ref _a;

        public Rectangle(Point3D a)
        {
            _a = a;
        }
    }

    public struct Point3D
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }

        public Point3D(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public double DoSomeCalculation()
        {
            return X * Y * Z;
        } 
}