Na ogół aby mogło dojść do skutecznego ataku, jego potencjalna ofiara musi posiadać pewne luki w zabezpieczeniach. Luki te najczęściej znajdują się albo w oprogramowaniu zlokalizowanym na systemie ofiary albo w konfiguracji tego oprogramowania. W tej prezentacji skupimy się przede wszystkim na błędach i niedociągnięciach programistów.
Największym, a zarazem najbardziej bolesnym w skutkach problemem spotykanym w ogromnej ilości aplikacji jest niedostateczne sprawdzanie danych wejściowych, które aplikacja pobiera bezpośrednio lub pośrednio od niezaufanego użytkownika. W niektórych przypadkach takie sprawdzenia są nawet całkowicie pomijane, najczęściej z powodu pośpiechu przy tworzeniu oprogramowania, niedbałego projektu, bałaganu w kodzie albo źle przeprowadzonych testów.
Skutki tego typu błędów mogą być bardzo dotkliwe. Na przykład zbyt długie dane wejściowe mogą doprowadzić do przepełnienia bufora lub wyczerpania zasobów, a złośliwie spreparowane dane mogą umożliwić wykonanie akcji, których programista się nie spodziewał (na przykład ataki typu injection), umożliwić użytkownikowi korzystanie z danych, do których nie powinien mieć dostępu a nawet zaatakować za naszym pośrednictwem innych (XSS - cross site scripting, wysyłanie spamu i wirusów z wcześniej pokonanych maszyn - tzw. zombie).
Należy pamiętać, że dokładnie sprawdzane powinny być nie tylko tradycyjnie pojmowane dane wejściowe (parametry programu, informacje które program wczytuje ze standardowego wejścia albo za pośrednictwem jakiegoś interfejsu użytkownika, parametry żądania HTTP i tak dalej) ale również ustawienia systemu (zmienne środowiskowe, konfiguracja systemu operacyjnego, bibliotek, serwera aplikacji i innych komponentów otoczenia programu) a nawet pewne ustawienia, które trafiają do programu niejawnie (dane sesji, uprawnienia zalogowanego użytkownika, zmienne globalne inicjalizowane przez parametry żądania HTTP w wielu wersjach PHP) lub pośrednio (na przykład informacje odczytane z bazy danych). Przy sprawdzeniach należy stosować zasadę wszystko co nie jest dozwolone jest zabronione to znaczy porównać wejście ze wszystkimi dozwolonymi wartościami a nie starać się znaleźć te które mogłyby być niebezpieczne dla programu w dalszych fazach przetwarzania i powinny być z tego powodu zabronione (ponieważ na pewno o czymś zapomnimy, a jak nie zapomnimy to ktoś za pół roku zmodyfikuje nasz program i wtedy zapomni dodać kolejne sprawdzenie).
Jeśli ktoś chciałby zobaczyć co dzieje się gdy całkiem pominiemy sprawdzanie danych wejściowych a przy okazji zrobimy kilka innych dziwnych rzeczy to polecam obejrzenie poniższego filmu (należy dwukrotnie nacisnąć przycisk odtwarzania).
Miejsce wszelkich sprawdzeń (danych wejściowych, uprawnień użytkownika i innych) powinno być określone przez projekt aplikacji i dokładnie opisane w jej dokumentacji. Należy też pamiętać, że odpowiednie podzielenie programu na warstwy i moduły, opracowanie dobrych protokołów komunikacji pomiędzy nimi oraz przygotowanie wygodnych i dobrze udokumentowanych interfejsów znacznie zmniejsza ryzyko powstania bałaganu, który jest głównym wrogiem bezpieczeństwa.
Przykładowo wszelkie sprawdzenia poprawności danych i uprawnień użytkowników do korzystania z nich powinny znajdować się jak najbliżej funkcji mających dostęp do tych danych (czyli w warstwie logiki biznesowej a nawet w warstwie bazy danych). Należy unikać umieszczania ostatecznych sprawdzeń w warstwie prezentacyjnej a już kompletnie niedopuszczalne jest zakładanie, że aplikacja jest prawidłowo chroniona ponieważ użytkownik nie zna odnośnika do jakiejś części funkcjonalności (na przykład modułu administratora albo skrzynki z cudzą korespondencją) lub na formularzu, który wyświetla mu się w przeglądarce brak przycisku do usunięcia ważnych informacji (ale jeśli wyśle odpowiednie spreparowane zapytanie do serwera to serwer je bez protestu wykona). Mogło by się wydawać, że tego typu błędy to rzadkość. Niestety, można je znaleźć w wielu aplikacjach, z których korzystamy na co dzień.
Jednak niedostateczne sprawdzanie danych wejściowych albo uprawnień to nie jedyne błędy, które atakujący może wykorzystać. Inną klasą problemów jest nadmierny determinizm programów oraz podatność na wyścigi. Tu należy się wyjaśnienie: wiele programów powinno zachowywać się deterministycznie w tym co robi, to znaczy zawsze dawać takie same rozwiązanie i tak dalej. Nie muszą one jednak, a nawet nie powinny, zachowywać się deterministycznie w innych aspektach, szczególnie tam, gdzie przewidzenie ich akcji może ułatwić złośliwemu użytkownikowi doprowadzenie do sytuacji wyścigu, która może być wykorzystana do jakichś niecnych celów.
Przykładem może być tworzenie przewidywalnych nazw plików tymczasowych, wielokrotnie już wykorzystywane w historii przez atakujących do nadpisania plików systemowych lub ataków typu DOS (ang. denial of service - uniemożliwienie dostępu do usługi) i podobnych. Jednak deterministyczne nazwy plików tymczasowych to nie wszystko. Często aplikacja potrzebuje nadawać pewnym zasobom numery (na przykład numer użytkownika, numer wiadomości, numer zgłoszenia i tak dalej). Jeśli te numery są nadawane kolejno to ktoś może z tego skorzystać na przykład do oceny ilu może być użytkowników albo zgłoszeń (ataki typu information leak - wyciek informacji) a nawet (na ogół w połączeniu z błędnym lub źle zaprojektowanym sprawdzaniem uprawnień) do przeglądania wszystkich wiadomości po kolei. Podobne błędy znajdowały się i znajdują nawet w systemach stosowanych na Wydziale. Dlatego, gdy już musimy nadać jakiś identyfikator, zastanówmy się nad użyciem liczby losowej lub odpowiednio bezpiecznej jednokierunkowej funkcji skrótu (typu SHA256, RIPEMD-160 i tym podobne).
Inne potencjalne błędy na które warto zwrócić uwagę projektując i implementując oprogramowanie to m.in. brak limitów alokacji zasobów (nagminnie spotykany w programach, szczególnie w językach wyższego poziomu posiadających wbudowane klasy łańcuchów, wektorów i kolekcji i łatwo prowadzący do ataków typu DOS) oraz nadmierne zaufanie do użytkownika i próby "ułatwiania mu życia" (na przykład poprzez dynamiczne zwiększanie limitów gdy są one zbyt małe do wykonania jakiejś akcji - co często prowadzi albo do dodatkowych błędów albo do niekontrolowanego użycia zasobów).