Secure programming/pl

From Free Pascal wiki
Jump to navigationJump to search

English (en) français (fr) polski (pl)

Bezpieczne programowanie

Przedmowa

Ta strona wiki jest próbą nauczenia innego podejścia do tworzenia oprogramowania. Strona wykorzystuje bardzo proste przykłady, aby pokazać, że wiele problemów można wykorzystać w celu wykonania ataku bezpieczeństwa na komputer, program lub cały system.

Należy pamiętać, że dokument jest tylko początkiem nauki pisania lepszego i nieco bezpieczniejszego kodu, ale nie stanowi kompletnego przewodnika, jak to zrobić. W rzeczywistości jest to tylko krótki przegląd tego, jak musimy widzieć nasz kod i program oraz jak uniknąć wielu typowych problemów, które tam występują.

Proszę pamiętać, że ten dokument dotyczy edukacji w celu lepszego kodowania, a nie przewodnika po hakowaniu i łamaniu programów.

Możesz być także zainteresowany zasadami The Power of 10, aby uzyskać bardziej zwięzłą poradę.

Nie ufaj danym wejściowym

Podczas tworzenia programu jest prawdopodobne, że będzie on w jakiś sposób wchodził w interakcję z użytkownikiem, nawet jeśli oznacza to jedynie odczytywanie plików w systemie i prezentację danych.

Zwykle w szkołach i na uniwersytetach, gdy zaczyna się pisać programy, osoba ta uczy się, jak otrzymywać dane wejściowe, podczas gdy nauczyciele zwykle mówią tej osobie „załóż, że dane, które otrzymujesz, są prawidłowe”. Wtedy zaczynają się problemy:

Light bulb  Uwaga: Nie możemy ufać żadnym danym wejściowym, których nie możemy kontrolować, ponieważ ich zawartość jest nieznana i może wykorzystywać lukę w naszym oprogramowaniu.

Czytanie z pliku to odczytywanie niezaufanych danych wejściowych, podobnie jak odczytywanie danych wejściowych użytkownika lub akceptowanie danych wejściowych z sieci.

Dlaczego nie mogę ufać danym wejściowym?

Aby zrozumieć, dlaczego dane wejściowe są niebezpieczne, najpierw musimy zrozumieć, czym są dane wejściowe.

Dane wejściowe mogą pochodzić z naciśnięcia klawisza, ruchu myszy lub kliknięcia przycisku myszy, lub z odczytywania i akceptowania informacji z wielu innych sposobów, takich jak strumień danych lub nawet funkcje systemowe. W rzeczywistości wszystko, co Twój program otrzymuje z zewnątrz, to są dane wejściowe.

Nie ma znaczenia, jaki jest rodzaj danych wejściowych: na przykład użytkownik lub inny system może podać nam błędne dane, a przyczyny mogą być celowe lub pomyłka. Nie możesz kontrolować tego wejścia, a głównym powodem jest to, że nie możesz odgadnąć, jakie będą to dane wejściowe.

Wynikiem mogą być puste (NULL) „dane”, które podaje nam użytkownik, liczba wykraczająca poza nasz oczekiwany zakres lub większa ilość znaków niż oczekiwaliśmy, a nawet próba zmiany adresu zmiennej, która akceptuje dane wejściowe od użytkownika. Po prostu nie wiemy, jakie dane wprowadzi użytkownik.

Każde „niebezpieczne” postępowanie z danymi wejściowymi może spowodować pobranie krytycznych informacji, których użytkownik nie może zobaczyć, lub modyfikację danych, do której użytkownik nie jest upoważniony, uszkodzenie danych, a nawet uszkodzenie (zawieszenie) samego programu.

Jakich problemów możemy się spodziewać?

Przy każdym typie błędu prawdopodobnie znajdziesz inny sposób ataku, ale chciałbym podać krótką listę bardzo powszechnych rodzajów ataków, zamiast opisywać wiele ich typów.

Najczęstsze rodzaje ataków to:

Przepełnienie bufora

Gdy otrzymane dane przepełnią ilość pamięci, która została dla nich przydzielona:

var
  iNums : array [0..9] of integer;
  ...
  FillChar (iNums[-1], 100, #0);
  ...
  for i := -10 to 10 do
  readln (iNums[i]);
  ...

Na tym przykładzie widać, że dla statycznej tablicy iNums daliśmy możliwość przyjęcia tylko 10 liczb, podczas gdy potem próbujemy wprowadzić 21 liczb.

Należy pamiętać, że chociaż kompilator może ostrzegać w prostych przypadkach, to nie zawsze będzie on reagował w bardziej rozbudowanych formach.

Jeśli użytkownik może wprowadzić dane, które są wysyłane do bufora, może on podać pewne wartości, które można zinterpretować jako instrukcje kodu maszynowego, a te zostaną zapisane poza naszym buforem. Komputer mógłby wtedy wykonać ten kod zamiast kodu, który powinien tam być. To jest własnie przepełnienie bufora.

Atak DoS

Denial of Service to nie tylko problem sieciowy, ale może istnieć również na wiele innych sposobów:

procedure Recurse;
begin
  while (True) do
    begin
      Recurse;
    end;
end;

Ta procedura będzie działać, dopóki system nie wyczerpie zasobów, ponieważ przy każdej rekursji przydziela więcej pamięci stosu i spowoduje, że system przestanie odpowiadać, a nawet ulegnie awarii. Chociaż niektóre systemy – takie jak Linux – będą próbowały dać ci możliwość zaprzestania uruchamiania programu, zajmie ci to dużo czasu.

Należy pamiętać, że jest to tylko statyczny przykład, ale przeprowadziliśmy atak DoS na system uruchamiając ten kod.

Innym znanym atakiem DoS jest brak zwolnienia zasobów systemowych, takich jak pamięć, gniazda, deskryptory plików itp.

Na przykład:

...
  begin
    while True do
      begin
        Getmem(OurPtr, 10);
        OurPtr := Something;
      end;
  end.

Ten przykład pokazuje alokację pamięci (Getmem jest jak malloc w C: rezerwuje pamięć do jakiegoś użycia), ale kończymy wykonywanie kodu bez zwalniania pamięci pod koniec jej używania.

Wstrzyknięcia

Kiedy użytkownik podaje nam dane wejściowe, a my pracujemy na danym wejściu bezpośrednio bez oczyszczania go, użytkownik może umieścić coś w niektórych tagach SQL lub kodzie (takim jak kod skryptu lub kod maszynowy), co sprawi, że nasz program będzie wykonywał pewne dodatkowe działania (np. usunięcie niektórych rekordów/tabeli, wysłanie użytkownikowi pewnych zastrzeżonych danych, takich jak struktura bazy danych/tabeli, użytkownik bazy danych i hasło, zawartość katalogu lub pliku, a nawet uruchomienie programu na komputerze).

Przykład wstrzyknięcia SQL:

Dane wejściowe użytkownika: Podaj swoje imię i nazwisko: a' OR 1=1

Wewnątrz kodu:

...
Write('Proszę, wpisać swoje imię: ');
ReadLn(sName);
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name='#32 + sName + #32);
...

Przesłanie tej instrukcji SQL jako danych wejściowych użytkownika spowoduje, że nasze zapytanie doda „ OR 1=1” do instrukcji SQL przekazanej do bazy danych: w tym przypadku zawsze skutkuje to prawdą, a użytkownik uzyskał nieautoryzowany dostęp do programu.

Dostęp do Twoich danych i modyfikacja

Nie tylko Twój program będzie miał dostęp do danych, z których korzysta. Jeśli przechowujesz dane w plikach lub (zdalnych) bazach danych, atakujący może uzyskać do nich dostęp za pośrednictwem systemu operacyjnego (i/lub bazy danych i/lub warstwy sieci/protokołu bazy danych).

Szyfrowanie: czy to wystarczy?

Aby przeciwdziałać opisanemu powyżej wątkowi, programiści często stosują szyfrowanie. Szyfrowanie może służyć do zapewnienia:

  • ochrony poufności danych
  • niezaprzeczalności (to dane stworzone przez osobę, która je stworzyła) i integralności (czy dane są niezmienione) (za pomocą dodatkowych mechanizmów, takich jak podpisy cyfrowe/haszowanie)

zarówno dla

  • przekazywania danych
  • przechowywania/odzyskiwania danych

Jeśli korzystasz z tych metod, Twoje dane nie są jednak automatycznie bezpieczne.

Istnieje wiele możliwych ataków na zaszyfrowane dane:

  • atak na niezabezpieczone algorytmy lub implementację (np. przy użyciu ataku ze znanym tekstem jawnym)
  • atak na klucze szyfrujące (np. poprzez odwrotną inżynierię Twojego programu, np. jeśli przechowuje klucze w pliku lub w sobie, lub poprzez łatanie programu, aby przechwycił klucze/hasła szyfrujące wprowadzane przez użytkownika).

Jeśli nie wiesz dokładnie, co robisz (i nie będziesz wiedzieć, chyba że masz przeszkolenie kryptograficzne), użyj (w kolejności malejącej preferencji):

  • dobrze znane biblioteki, które są utrzymywane/łatane (np. wbudowane biblioteki FPC i biblioteki takie jak DCPCrypt lub biblioteki zewnętrzne, takie jak openssl lub cryptlib), które używają dobrze znane/powszechnie używane protokoły, i które używają dobrze znane/powszechnie używane algorytmy
    • Użyj np. zaufane uwierzytelnianie/SSPI dla połączeń z bazą danych SQL Server/Firebird, aby uniknąć wysyłania haseł przez sieć (Firebird 2 i niższe: w postaci zwykłego tekstu!) i polegać na bezpieczeństwie systemu operacyjnego do uwierzytelniania.
    • użyj cryptlib lub openssl z Synapse do wdrożenia SSL/TLS zamiast rozwijania własnego rozwiązania
  • dobrze znane/powszechnie używane protokoły/API zamiast rozwijania własnych. Protokoły te muszą wykorzystywać dobrze poznane/szeroko stosowane algorytmy kryptograficzne/haszujące. Przykłady:
    • GPG/PGP
    • TLS (SSL) z PKI/CA (najlepiej nie ufaj tylko tym urzędom, których potrzebujesz i wykonuj uwierzytelnianie za pomocą certyfikatu klienta, jeśli wymaga tego analiza zagrożeń)
    • SSH (np. z uwierzytelnianiem klucza publicznego/prywatnego, w razie potrzeby wzmocniony hasłami do kluczy)
    • w systemie Windows użyj interfejsu API, aby uzyskać aktualnie uwierzytelnionego użytkownika. Jeśli wymuszasz odpowiednie zabezpieczenia systemu operacyjnego (długość hasła, zmiany, dostęp fizyczny itp.), nie musisz zarządzać własnym mechanizmem ogsługi nazwy użytkownika/hasła na poziomie aplikacji.
  • dobrze poznane algorytmy kryptograficzne/haszujące (takie jak AES/Rijndael, 3DES i SHA512). Zachowaj ostrożność w zakresie akceptowanych algorytmów (np. MD5 jest niezabezpieczony do podpisywania wiadomości).

Szyfrowanie jest częścią zestawu możliwych środków bezpieczeństwa; wysiłek/pieniądze na to wydane powinny zostać ocenione w ramach analizy bezpieczeństwa (patrz poniżej).

Mity i założenia

Wiele problemów związanych z bezpieczeństwem wynika z ignorowania ważnych ostrzeżeń i informacji przekazanych przez kompilator oraz z myślenia, że ​​program nie zawiera żadnego możliwego do wykorzystania problemu.

Oto kilka przykładów tego typu problemów:

mity

  • Bezpieczeństwo przez ukrycie – Gdy nikt nie wie o problemie, nikt nie może z niego skorzystać; np. użyj niejasnej nazwy kolumny do przechowywania haseł w swojej bazie danych.
  • Bezpieczny język programowania – Istnieją języki takie jak Perl, które wielu ludzi uważa za bezpieczne przed przepełnieniem bufora i innymi lukami, choć nie jest to prawdą.
  • Hasło haszowane jest bezpieczne — plik z haszowanym hasłem nie jest bezpieczny. Hash może odpowaidać tylko jednej wartości i nie można odzyskać oryginalnych danych. Ale „zahaszowane hasło może zostać odzyskane za pomocą ataku typu brute force lub tęczowej tablicy i dlatego wymaga użycia soli lub wielu rund haszujących”: --BigChimp 16:19, 24 July 2011 (CEST)
  • Nic nie może zepsuć mojego programu – przekonanie, że jesteś jedynym programistą na świecie, który pisze bezbłędny kod, jest prawdopodobnie zbyt optymistyczne. Może masz szczęście i po prostu piszesz kod, który nie działa poprawnie bez luk w zabezpieczeniach, które można wykorzystać...

założenia

  • Zespół QA znajdzie i naprawi moje błędy bezpieczeństwa.
  • Użytkownik (lub ktoś inny) nie zaatakuje mojego programu i jego danych.
  • Mój program będzie używany tylko do pierwotnego użytku.
  • Wszystkie wyjątki mogą pozostać nieobsłużone.

Analizuj, aby zrozumieć zagrożenia i bezpieczeństwo

Generalnie programista (lub jego pracodawcy/właściciele firmy) powinien przeprowadzić analizę wszystkich zagrożeń (od fizycznego dostępu/ataku po ataki logiczne i socjotechniczne) dla całego systemu (w tym infrastruktury – OS, bazy danych, sieci a także fizycznej maszyny/okablowanie/budynki/połączenia zewnętrzne), aby przeanalizować, czy nie pozostawiasz otwartej luki w zabezpieczeniach, która jest niedopuszczalna (z perspektywy ryzyka/korzyści).

Zakres analizy powinien zależeć od wartości danych/procesów chronionych przez system. Oczywiście nie ma sensu szaleć, próbując przeanalizować fizyczne bezpieczeństwo swojego domu podczas opracowywania programu hobbystycznego, aby śledzić wyniki brydżowe.

Korzyści z tego rodzaju analiz:

  • zagrożenia pozostające po ogłoszeniu lub ujawnieniu środków bezpieczeństwa. Często toczy się dyskusja na temat prawdopodobieństwa wystąpienia tego ryzyka lub związanego z nim wpływu, ale fakt, że istnieje jakiś potencjalny wektor ataku sprawia, że jest on przynajmniej jasny i można podejmować decyzje na podstawie tych informacji
  • dość łatwo można zobaczyć, jakie środki bezpieczeństwa są przeprojektowane („zbyt bezpieczne”, marnotrawstwo pieniędzy) lub niedopracowane („niewystarczająco bezpieczne”). Jeśli nie możesz teraz wykorzystać tych informacji, możesz przynajmniej uczyć się z nich dla innych projektów, w tym przyszłej konserwacji/modyfikacji programu

Konkretne rozwiązania

Skoro znamy już pewne problemy, które możemy napotkać podczas tworzenia programów, powinniśmy dowiedzieć się, jak je naprawić. Wszystkie problemy, które widzieliśmy powyżej, przejawiają się w dwóch typach: założenia i brak starannego programowania. A żeby nauczyć się je naprawiać, najpierw musimy nauczyć się myśleć w inny sposób niż dotychczas.

Przepełnienie

Aby naprawić przepełnienie danych, takie jak bufory i inne rodzaje danych wejściowych, musimy przede wszystkim zidentyfikować rodzaj danych, z którymi musimy pracować.

Przepełnienie bufora

Jeśli wrócimy do naszego przykładu:

var
  iNums: array [0..9] of Integer;
  ...
  FillChar(iNums[-1], 100, #0);
  ...
  for i := -10 to 10 do
    ReadLn(iNums[i]);
  ...


Widzimy tutaj zakres, który został przepełniony naszymi wartościami, nawet nie sprawdzając, czy numer indeksu jest poprawny.

W tablicach dynamicznych/otwartych w Pascalu możemy poznać limity przydzielonej pamięci. Więc wszystko, co musimy zrobić, to sprawdzić, czy rozmiar nie jest za mały lub za duży dla naszego bufora i ograniczyć akceptację do rozmiaru, jaki sobie życzymy.

Zatem przykład należy zmienić na:

var
  iNums: array [0..9] of Integer;
  
  ...
  FillChar (iNums[Low(iNums)], High(iNums), #0);
  ...
  
  for i := Low(iNums) to High(iNums) do
    ReadLn(iNums[i]);
  ...

Ale czekaj, coś jest jeszcze nie tak!

Readln przyjmie nieograniczoną ilość znaków i nikt nam nie obiecuje, że będzie to liczba całkowita lub nawet z zakresu, który możemy obsłużyć.

Przepełnienie liczby

Podczas gdy łańcuch w Pascalu jest czystą tablicą (hrmm hrmm.. niezupełnie, przynajmniej nie w FPC, ale załóżmy przez chwilę, że jest, OK?), więc readln spróbuje znaleźć i zobaczyć, jakie są jego limity i czy nie spróbujemy przekroczyć zakresu, który nadaliśmy temu typowi, ale liczby nie są takie same.

Liczby mają ograniczenia, komputer/kompilator ma wiele ograniczeń dotyczących pamięci i liczb. Może dać tylko „małą” ilość pamięci na liczby (liczby zmiennoprzecinkowe i całkowite). I w większości przypadków nie potrzebujemy uzywać dużego zakresu liczb (jak zmienna logiczna, która zwykle potrzebuje tylko dwóch liczb).

W powyższym przykładzie możemy mieć przepełnienie bufora, które spowoduje błąd sprawdzania zakresu, który da nam złą liczbę (problemy z przypomnieniem Carry Flag (flaga przeniesienia)... nie będę ich tutaj wyjaśniać), a także mamy efekt DoS, ponieważ nasz program zatrzyma się w tym momencie.

Więc co możemy zrobić, aby to naprawić?

Przede wszystkim możemy chcieć pracować ze zmienną łańcuchową, która będzie odpowiadała największej długości liczby +1 (dla znaku minus) lub możemy stworzyć własną procedurę/funkcję readln, która będzie specjalizować się w typie integer.

Dla pierwszej opcji możemy wykonać następujące czynności (skopiowane z dokumentacji FPC):

Program Example74;

{ Program do demonstracji funkcji Val. }
Var
  I, Code: Integer;
begin
  Val(ParamStr(1), I, Code);
  If Code <> 0 then
    Writeln('Błąd na pozycji ', code, ' : ', Paramstr(1)[Code])
  else
    Writeln('Wartość : ', I);
end.

Tutaj widzimy, jak przekonwertować łańcuch na liczbę całkowitą z bardzo łatwą obsługą błędów. Funkcja StrToInt także może załatwić sprawę, ale wtedy musimy przechwycić wyjątek w przypadku obsługi błędów.


Oto mały przykład małej procedury typu readln dla liczb całkowitych.

program MyReadln;
uses
  CRT;

procedure MyIntReadLn(var Param: Integer; ParamLength: Integer);
var
  Line: string;
  ch: char;
  Error: Integer;
begin
  Line  := '';
  
  repeat
    ch := ReadKey;
    if (Length (Line) <> ParamLength) then
    begin
      if (ch in ['0'..'9']) then
      begin
        Line := Line + ch;
        write (ch);
      end
      else
      if (ch = '-') and (Length(Line) = 0) then
      begin
        Line := '-';
        write (ch);
      end;
    end;
    
    if (ch = #8) and (Length(Line) <> 0) then // backspace
    begin
      Line := Copy(Line, 1, Length(Line) - 1);
      gotoxy(WhereX - 1, WhereY);
      write(' ');
      gotoxy(WhereX - 1, WhereY);
    end;
  until (ch = #13);
  
  val(Line, Param, Error);
  
  if (Error <> 0) then
    Param := 0;
  
  writeln;
end;

var
  Num : Integer;
begin
  Write('Number: ');
  MyIntReadLn(Num, 2);
  WriteLn('The number is: ', Num);
end.

Pamiętaj, że możesz uczynić go jeszcze lepszym i bardziej wydajnym, jeśli chcesz. To tylko bardzo mały przykład pokazujący, jak to zrobić.

Jakie są zagrożenia bezpieczeństwa w Przepełnieniach?

Przepełnienie pamięci może pozwolić na wykonanie dowolnego kodu procesora, a użytkownicy mogą uruchamiać dowolny typ kodu i nic nie może ich powstrzymać.

Blokada usług DoS

Blokada usług (DoS) to jeden z najtrudniejszych rodzajów ataków, którym należy zapobiec. Powody to:

  • Blokada usług może być wykonana nawet bez błędów, które można wykorzystać, na przykład przy użyciu programu „ping” na wielu komputerach w celu DoS komputera podłączonego do Internetu.
  • Każdy zasób systemowy może być ofiarą DoS, taką jak otwieranie gniazd, odczytywanie plików lub przydzielanie pamięci.
  • Usunięcie plików takich jak moduł jądra może spowodować duży problem. Nie rozumiem tego wiersza --BigChimp 19:32, 24 lipca 2011 (CEST)
  • Brak konfiguracji lub niewłaściwa konfiguracja może również spowodować blokadę usług, jeśli pozwala to na niewłaściwe wykorzystanie wrażliwych zasobów.
  • Za dużo uprawnień lub ich brak Nie rozumiem. Niewystarczająca liczba uprawnień może stanowić problem. Ale za dużo? To oczywiście zagrożenie bezpieczeństwa, ale nie problem DoS. --BigChimp 19:32, 24 lipca 2011 r. (CEST).
  • Prawie każdy rodzaj exploita może spowodować blokadę usług.

Jak widać, blokada usług może być prawie wszystkim, co może uniemożliwić systemowi działanie tak, jak powinien, z powodu wykorzystania błędów lub błędnego kodu lub po prostu wadliwego programu, który przechwytuje zasoby systemowe.

W powyższym przykładzie DoS:

procedure Recurse;
begin
  while (True) do
    begin
      Recurse;
    end;
end;

Stworzyłem również przepełnienie stosu (inny rodzaj przepełnienia bufora), który spowodował, że komputer potrzebował więcej zasobów pamięci, aby kontynuować wykonywanie kodu.

Każdy zasób systemowy, który jest dostępny dla programu, może zostać nadużyty, nie zwracając go z powrotem do systemu, gdy program „już go nie potrzebuje”. Przetrzymywanie zasobów systemowych, takich jak pamięć lub gniazda, blokuje innym programom możliwość wykonywania niektórych z ich działań. W ten sposób większość programów zatrzyma wykonywanie i zgłosi błąd, a niektóre będą się zawieszać i nadal szukać zasobów systemowych.

Należy pamiętać, że niektóre nadużycia zasobów systemowych wynikają z błędu w programowaniu, jak oczekiwanie na bufor 150k, podczas gdy rzeczywisty bufor ma tylko 2 bajty, a gdy program nadal szuka bufora 150k, nowe żądanie tworzy kolejny bufor 150k itd., dopóki system nie będzie już w stanie odpowiedzieć na żadne z żądań (jest to znany typ ataku).

Dobrym obejściem tego błędu jest ograniczenie liczby niepełnych buforów, które można przydzielić jednocześnie. Jeśli bufor nie jest pełny po upływie limitu czasu, powinien być wolny. Jednak to rozwiązanie spowoduje również odmowę usługi, ponieważ komunikacja i tak zostanie zatrzymana w pewnym momencie lub powolne połączenie może spowodować utratę danych.

Wstrzykiwanie

Istnieje wiele sposobów na wstrzyknięcie pewnego rodzaju kodu do naszych programów. Jak widzieliśmy na powyższym przykładzie:

Dane wejściowe użytkownika: Podaj swoje imię i nazwisko: a' LUB 1=1

Wewnątrz kodu:

...
write('Proszę wpisać swoje imię: ');
readln(sName);
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name='#32 + sName + #32);
...

Wstrzyknięcie nastąpiło, ponieważ nie filtrujemy (nie odkażamy) naszego kodu: oznacza to sprawdzenie, czy otrzymujemy dokładnie taki rodzaj wejścia, jakiego szukamy, i nic więcej.

Na przykład możemy sprawdzić, czy sName zawiera spacje. Jeśli tak, nie kontynuuj sprawdzania reszty zmiennej. To pomaga, jeśli nazwa użytkownika może składać się tylko z jednego słowa składającego się z liter, może znaku apostrofu (') i może nawet podkreślenia (_) i na tym koniec. Jeśli wprowadzimy liczbę, powinno to być nielegalne (chyba że chcemy używać „języka hackerskiego” (fonetycznie) lub zezwolić na używanie cyfr.

Istnieje wiele sposobów oczyszczenia danych. Mniej skuteczny (ale często używany) to:

Nieskuteczne odkażanie

function ValidVar (const S: AnsiString; AllowChars: TCharset): Boolean;
var
  i: Word;
begin
  i := 0;
  Result := True;
  
  while (Result) and (i <= Length(S)) do
  begin
    Inc(i);
    Result := S[i] in AllowChars;
  end;
end;

Funkcja zwraca wartość true, jeśli mamy poprawną strukturę treści podaną przez parametr AllowChars w zmiennej S. Należy pamiętać, że ta funkcja jest tylko próbką koncepcji i może wymagać więcej pracy, aby mogła zostać w pełni wykorzystana.

Innym sposobem na zrobienie tego samego jest użycie wyrażeń regularnych w następujący sposób (jest to próbka koncepcji wzięta z języka Perl. FPC nie ma w pełni obsługiwanego silnika wyrażeń regularnych, który umożliwia modyfikację ciągów):

$sName =~ s/[^a-z0-9\_\']//gi;

Wyrażenie regularne usuwa wszelkie niepoprawne znaki z łańcucha i zwraca do nas usunięty łańcuch. Zwróć uwagę, że o ile wiem, to wyrażenie regularne będzie działać również w silnikach ereg, ale z minimalnymi korektami (flaga g instruuje Perla, aby zastąpił wszystkie znalezione pasujące wzorce, a i oznacza case insensitivity (niewrażliwość na wielkość liter)).

Teraz, gdy wiemy, że nasze dane wejściowe są prawidłowe, musimy zobaczyć, do czego służy zawartość zmiennej. Jeśli zawartość zmiennej trafia do bazy danych, skryptu cgi lub czegokolwiek innego, co ma własną składnię, musimy zmienić treść zgodnie z niedozwolonymi lub kontrolnymi znakami odpowiedniego języka (np. SQL dla baz danych).

Istnieje wiele sposobów na uniknięcie tego typu zawartości. Załóżmy na razie, że ta treść trafia do zapytania bazy danych. Teraz przede wszystkim musimy upewnić się, że nasz dodatkowy znak ucieczki, nie zwiększy rozmiaru naszych danych, powyżej limitu długości pól w naszej bazie danych. Ponieważ, jeśli tak się stanie, możemy doprowadzić do wstrzykiwania, do utraty danych, blokady usług lub przepełnienia bufora (poważna baza danych zwykle obcina dane, ale czasami nie we właściwym miejscu).

Zwykle jedyną ucieczkę, jaką musimy zrobić, aby użyć ciągu w bazie danych, jest ucieczka jedynie znaku apostrofu (') (chociaż niektóre bazy danych mogą mieć problemy z większą liczbą znaków niż apostrof). Tak więc wszystko, co powinniśmy zrobić, to reprezentować apostrofy w sposób, który nie wpłynie na silnik bazy danych, jak znak odwrotnego ukośnika (\') lub podwajać każdy apostrof do dwóch apostrofów (''), a może nawet użyć innego znaku, który zastąpi apostrofy w zapytaniu i zamienić je ponownie, gdy musimy pokazać je użytkownikowi.

Ograniczanie wprowadzania przez parametry zapytania SQL

Obowiązkowa ilustracja wizualna.

Po upewnieniu się, że przestrzegamy ograniczeń, możemy kontynuować nasze próby. Aby uciec od kodu, możemy użyć kilku podejść. Mniej przyjazny dla debugowania sposób, ale pewny sposób na poprawną ucieczkę, to użycie techniki parametrów:

Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name=?');
Query1.Parameters.Add(sName);
if (Query1.Execute) then
...

Ta technika pozwala silnikowi bazy danych uciec przed parametrem w taki sposób, abyśmy mogli korzystać z zawartości bez żadnych problemów związanych z niedozwolonymi znakami. Ponadto niektóre bazy danych mają zwiększoną wydajność w przypadku powtarzających się wywołań tego kodu, ponieważ mogą one przygotować do tego instrukcję wewnętrzną z parametrami. Wadą jest to, że nigdy nie możemy debugować wyniku zapytania. Oznacza to, że nie możemy zobaczyć, w jaki sposób zawartość sName osadzona jest w instrukcji SQL i nigdy nie możemy z tego powodu sprawdzić, czy nasze zapytanie było poprawne.

Jednak po przetestowaniu zapytania bez parametrów dodawanie parametrów jest dość łatwe, więc w praktyce ten problem nie jest tak duży, jak się wydaje.

Wydajny kod

Wspomniane powyżej środki bezpieczeństwa komplikują kod, jeśli użyłeś najbardziej wydajnego kodu, aby uzyskać potrzebną funkcjonalność. Na szczęście czasami możesz przekazać komplikacje kodu do frameworka/biblioteki, która się tym zajmuje. Na przykład: w przykładzie SQL DoS możesz pozwolić silnikowi bazy danych zająć się ucieknięciem danych za pomocą sparametryzowanych zapytań, przy niewielkim dodatkowym koszcie konieczności użycia parametrów w kodzie.

Jednak pisanie wydajnego, ale podatnego na zagrożenia kodu nie pomaga nikomu poza prawnikami od odpowiedzialności cywilnej i ekspertami kryminalistyki.

Poza tym dokumentem

O ile w tym dokumencie podałem kilka krótkich (tak, wiem, że to niedopowiedzenie 😉) przykładów i informacji o tym, jak stworzyć lepszy kod, o tyle jest wiele kwestii, których nie poruszyłem w tym dokumencie. Część z nich to uprawnienia użytkownika do uruchamiania programów, rootkity systemu i inne problemy, które nasz kod musi wziąć pod uwagę (zmienna środowiskowa to tylko jeden przykład).

Przeczytaj więcej artykułów dotyczących problemów z bezpieczeństwem, takich jak

Przepełnienia bufora:

Blokada usług (DoS):

Wstrzykiwanie SQL: