To jest właśnie różnica między zwykłym przeglądem kodu a audytem bezpieczeństwa. Ten drugi nie pyta "czy kod jest ładny", tylko "gdzie tu jest dziura, przez którą ktoś się wślizgnie". A jako lider techniczny masz idealną pozycję, żeby zaszczepić to myślenie w zespole - zanim zrobi to za Ciebie ktoś z zewnątrz, w znacznie mniej przyjemnych okolicznościach.
W tym artykule pokażę Ci, na co zwracać uwagę przeglądając kod C# pod kątem bezpieczeństwa, jakie narzędzia zrobią sporą część roboty za Ciebie oraz kiedy warto sięgnąć po wsparcie zewnętrznych ekspertów.
Na co zwracać uwagę w kodzie C#?
Przegląd bezpieczeństwa skupia się na wychwytywaniu fragmentów, które mogą prowadzić do podatności. Oto obszary, które warto trzymać na radarze.
1. Walidacja danych wejściowych
Zasada numer jeden: nigdy nie ufaj danym od użytkownika. Nawet jeśli masz walidację po stronie front-endu, musisz walidować i sanitizować wejście również po stronie serwera - front-end to tylko wygoda dla użytkownika, nie mechanizm bezpieczeństwa (napastnik po prostu ominie przeglądarkę). Brak odpowiednich sprawdzeń otwiera drogę do ataków wstrzyknięcia, takich jak SQL Injection czy XSS.
W praktyce sprawdzaj, czy kontrolery ASP.NET faktycznie korzystają z walidacji modelu:
[HttpPost]
public IActionResult Create(UserDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
/* ... dalsze przetwarzanie dopiero po walidacji */
}Zwróć też uwagę, czy dane trafiające do HTML są odpowiednio escape'owane. Razor domyślnie escape'uje wyjście, ale w momencie, gdy w kodzie pojawia się @Html.Raw(...) albo ręczne budowanie HTML-a ze stringów - zapala się lampka.
2. Zapytania SQL i operacje na bazie danych
To klasyk, który wciąż zbiera żniwo. Jeśli w kodzie widzisz konkatenację stringów budującą zapytanie - reaguj natychmiast:
/* ❌ Źle - prosta droga do SQL Injection */
var sql = "SELECT * FROM Users WHERE Id = " + userId;
var command = new SqlCommand(sql, connection);Wystarczy, że ktoś w miejsce userId poda 1; DROP TABLE Users; -- i masz problem. Poprawne podejście to zawsze zapytania parametryzowane:
/* ✅ Dobrze - parametr zamiast surowej wartości */
var sql = "SELECT * FROM Users WHERE Id = @userId";
var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@userId", userId);W praktyce najczęściej i tak korzystasz z ORM-a (Entity Framework Core, Dapper), które domyślnie parametryzują zapytania. Uwaga jednak na metody typu FromSqlRaw czy interpolację stringów - tam łatwo z powrotem otworzyć furtkę. Warto wiedzieć, że statyczna analiza .NET ma dedykowaną regułę CA2100, która ostrzega, gdy polecenie SQL powstaje na bazie wejścia użytkownika.
3. Uwierzytelnianie i autoryzacja
Sprawdź, czy miejsca wymagające uprawnień są faktycznie chronione. Na poziomie kontrolerów i metod API w ASP.NET Core powinny pojawiać się atrybuty [Authorize], a w logice - ograniczenia dostępu (np. użytkownik modyfikuje tylko własne dane, chyba że jest administratorem):
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult DeleteUser(int id) { /* ... */ }Najczęstsza dziura to nie brak logowania, lecz brak autoryzacji na poziomie zasobu. Endpoint wymaga zalogowania, ale nie sprawdza, czyje dane użytkownik próbuje odczytać lub zmienić - i nagle zalogowany klient A pobiera fakturę klienta B, podmieniając ID w URL-u. Szukaj takich "otwartych po zalogowaniu" endpointów świadomie.
4. Kryptografia i przechowywanie sekretów
Przejrzyj kod pod kątem używanych algorytmów. Przestarzałe lub słabe konstrukcje: MD5, SHA1, TripleDES - są uznawane za podatne i nie powinny trafiać do nowego kodu. Do haszowania haseł stosuj bcrypt lub PBKDF2 (np. Rfc2898DeriveBytes), a do szyfrowania - AES lub bibliotekę wysokiego poziomu, która zadba o poprawną konfigurację.
Osobny, poważny grzech to twardo zakodowane sekrety:
/* ❌ Źle - sekret w kodzie, wersjonowany razem z repozytorium */
var secretKey = "SuperSecretKey123";Jeśli znajdziesz coś takiego, wynieś to natychmiast poza kod. Sekrety powinny żyć w bezpiecznej konfiguracji - User Secrets (dev), zmiennych środowiskowych lub usługach typu Azure Key Vault - tak, by nie wyciekły razem z kodem. Dobre pytanie kontrolne na code review brzmi: "czy gdyby to repozytorium wyciekło jutro na GitHuba, coś by nam się zapaliło?".
5. Obsługa wyjątków i logowanie błędów
To, jak aplikacja reaguje na błędy, też jest kwestią bezpieczeństwa. Unikaj pustych bloków catch - "połykając" wyjątek, ukrywasz problem przed sobą, ale nie przed atakującym:
/* ❌ Źle - wyjątek zniknął bez śladu */
try { ProcessPayment(order); }
catch { }
/* ✅ Lepiej - świadoma obsługa i log */
try
{
ProcessPayment(order);
}
catch (PaymentException ex)
{
_logger.LogError(ex, "Płatność nie powiodła się dla zamówienia {OrderId}", order.Id);
throw;
}Druga rzecz: nie ujawniaj wrażliwych szczegółów w komunikatach błędów. Użytkownik powinien zobaczyć ogólne "Wystąpił błąd, spróbuj ponownie później", a pełny stack trace, ścieżki plików czy struktura bazy trafiają wyłącznie do logów dla deweloperów. Inaczej sam podasz atakującemu mapę systemu.
6. Niebezpieczne funkcje i operacje
Pewne wywołania z natury wymagają wzmożonej czujności:
• Process.Start z danymi od użytkownika - ryzyko wstrzyknięcia komend systemowych.
• Dynamiczne wykonywanie kodu, np. Eval w widokach Razor - może prowadzić do uruchomienia niepożądanego kodu po stronie serwera.
• Kod unsafe, wskaźniki, P/Invoke - błędy w zarządzaniu pamięcią potrafią zamienić się w poważne podatności.
Reguła kciuka: wszędzie tam, gdzie kod "wychodzi na zewnątrz" - odwołuje się do systemu operacyjnego, uruchamia procesy, ładuje niezweryfikowane dane - upewnij się, że po drodze jest solidna walidacja.
Narzędzia, które zrobią część roboty za Ciebie
Ręczny przegląd jest niezastąpiony, ale nie musisz łapać wszystkiego gołym okiem. Statyczna analiza wychwyci sporo typowych błędów automatycznie.
Wbudowane analizatory .NET (Roslyn Analyzers). Upewnij się, że są włączone w projektach - np. przez pakiet Microsoft.CodeAnalysis.NetAnalyzers i ustawienie <AnalysisMode> w pliku projektu. Reguły takie jak wspomniana CA2100 (SQL Injection) czy CA2153 (niepoprawna obsługa Corrupted State Exceptions) alarmują już na etapie kompilacji. Jeśli któraś reguła jest wyłączana, powinien za tym stać naprawdę dobry powód opisany w kodzie.
Dodatkowe skanery bezpieczeństwa. SecurityCodeScan (SCS) wykrywa m.in. twardo zakodowane sekrety, słabą kryptografię (MD5/SHA1) oraz podejrzane wywołania API jak Process.Start czy Eval. Microsoft DevSkim (rozszerzenie do VS Code) wyłapuje wzorce podatności w locie - od słabych regexów po niebezpieczne ustawienia protokołów. Wpięcie ich w IDE i pipeline CI/CD sprawia, że oczywiste wpadki są odsiewane, zanim kod trafi na produkcję - a Ty skupiasz się na trudniejszych przypadkach.
Platformy CI i narzędzia enterprise. Przy większych projektach warto rozważyć SonarQube / SonarCloud (błędy, code smells i podatności podczas builda) oraz komercyjne skanery SAST - Checkmarx, Fortify, Veracode - z raportami zgodnymi z CWE czy OWASP Top 10. Rozwiązania enterprise bywają kosztowne, ale w branżach regulowanych (finanse, medycyna) często się zwracają.
Niezależnie od wyboru - automatyzuj. Uruchamiaj skanery w pipeline, a podatności w zależnościach monitoruj na bieżąco:
dotnet list package --vulnerableTo jedno polecenie pokaże znane luki w używanych pakietach NuGet.
Kiedy zaangażować zewnętrzny audyt?
Nawet najlepszy zespół ma "tunelowe" spojrzenie na własny kod - patrzysz na niego tyle razy, że przestajesz widzieć problemy. Dlatego w kluczowych momentach warto wpuścić świeże oko z zewnątrz.
Szczególnie:
• przed wydaniem krytycznego modułu - obsługującego płatności, dane osobowe, kluczowe procesy biznesowe;
• gdy produkt musi spełniać normy typu PCI DSS lub ISO 27001, gdzie niezależny audyt bywa wymagany lub silnie zalecany.
Zewnętrzni specjaliści łączą podejście white-box (przegląd kodu źródłowego) z black-box (testy penetracyjne działającej aplikacji). Warto wiedzieć, że audyt kodu źródłowego potrafi wykryć więcej podatności niż same testy "od zewnątrz" - bo widzi to, czego z poziomu interfejsu po prostu nie da się dostrzec. Jeśli bezpieczeństwo jest dla firmy priorytetem, taka inwestycja co jakiś czas po prostu się opłaca.
Ściąga: szybki checklist do code review
Wrzuć to do szablonu pull requesta albo powieś zespołowi przed oczami:
-Dane wejściowe są walidowane po stronie serwera (ModelState.IsValid)?
-Zapytania SQL są parametryzowane - zero konkatenacji stringów?
-Endpointy mają [Authorize] i sprawdzają dostęp do konkretnego zasobu?
-Brak słabej kryptografii (MD5/SHA1/TripleDES); hasła przez bcrypt/PBKDF2?
-Żadnych sekretów w kodzie - wszystko w konfiguracji / Key Vault?
-Brak pustych catch; błędy logowane, użytkownik widzi ogólny komunikat?
-Podejrzane wywołania (Process.Start, Eval, unsafe) są uzasadnione i zabezpieczone?
-Analizatory włączone, dotnet list package --vulnerable czysty?
Podsumowanie
Regularny audyt bezpieczeństwa .NET to nie jednorazowa akcja, tylko sposób myślenia, który systematycznie podnosi poziom Waszego produktu. Jako lider możesz dzięki niemu nie tylko wyłapać błędy przed kimś z zewnątrz, ale też zbudować w zespole prawdziwą kulturę security-first. Bezpieczeństwo to proces ciągły - nowe zagrożenia, aktualizacje zależności, wymiana wiedzy w zespole. To się nigdy nie "kończy", i o to właśnie chodzi.
Zostańmy w kontakcie
Bezpieczeństwa w .NET nie da się domknąć jednym artykułem - to temat, który wciąż się zmienia. Dlatego regularnie wysyłam programistom konkretne, praktyczne wskazówki o bezpiecznym kodowaniu w .NET - takie, które od razu przełożysz na własny kod, bez lania wody i bez spamu.
Jeśli chcesz je dostawać jako pierwszy, dołącz do listy VIP: modestprogrammer.pl/vip
To najprostszy sposób, żeby być na bieżąco i systematycznie podnosić poziom swoich aplikacji. Do zobaczenia po drugiej stronie - i powodzenia w zabezpieczaniu kodu.