SQL Injection i parametryzacja w .NET — bezpieczne zapytania

Sklejanie zapytania SQL ze stringów to najczęstsza krytyczna podatność w aplikacjach (OWASP od lat trzyma injection w czołówce) i pewne pytanie na każdej rozmowie dotykającej bazy danych. Wystarczy jeden apostrof w danych od użytkownika, żeby zamienić „zaloguj tego użytkownika” w „wypisz wszystkie hasła”. Umiesz już pisać zapytania — teraz naucz się wysyłać je z aplikacji tak, żeby dane użytkownika nigdy nie stały się kodem.
Jak działa SQL injection
Załóżmy logowanie, które skleja zapytanie ze stringów:
// ❌ NIGDY tak nie rób
var sql = "SELECT * FROM Users WHERE Login = '" + login + "'";Dla normalnego loginu anna powstaje poprawne zapytanie. Ale gdy użytkownik wpisze w pole login:
' OR '1'='1zapytanie staje się:
SELECT * FROM Users WHERE Login = '' OR '1'='1''1'='1' jest zawsze prawdziwe — warunek przepuszcza wszystkich użytkowników. Bardziej agresywny ładunek ('; DROP TABLE Users;--) potrafi usunąć dane. Problem jest jeden: dane użytkownika zostały wklejone jako kod SQL, zamiast być traktowane jako wartość.
Injection nie bierze się z „złego użytkownika”. Bierze się z jednego znaku
+sklejającego dane z zapytaniem. Usuń sklejanie, a atak znika u źródła.
Parametryzacja — dane osobno od zapytania
Rozwiązanie to parametry: wysyłasz szkielet zapytania z symbolami zastępczymi i osobno wartości. Baza traktuje wartości wyłącznie jako dane — apostrof w nich jest zwykłym znakiem, nie końcem stringa.
### ADO.NET (surowo)
// ✅ parametr, nie sklejanie
var cmd = new SqlCommand(
"SELECT * FROM Users WHERE Login = @login", connection);
cmd.Parameters.AddWithValue("@login", login);@login to placeholder; login jedzie osobno. Nawet ' OR '1'='1 zostanie potraktowane jako dosłowna wartość do porównania — i nic nie znajdzie.
### Dapper
Dapper parametryzuje automatycznie na podstawie obiektu — dlatego jest domyślnie bezpieczny:
var user = connection.QuerySingleOrDefault<User>(
"SELECT * FROM Users WHERE Login = @Login",
new { Login = login }); // wartość jako parametr, nie stringWięcej o tym podejściu w budowaniu API na Dapperze.
### Entity Framework Core
LINQ jest bezpieczny z definicji — EF Core zawsze parametryzuje wygenerowany SQL:
var user = db.Users.FirstOrDefault(u => u.Login == login); // parametr pod spodemPoza bezpieczeństwem parametryzacja daje bonus wydajnościowy: baza cache’uje plan wykonania dla szkieletu zapytania i używa go ponownie dla różnych wartości, zamiast kompilować każde sklejone zapytanie od nowa.
Transakcje — wszystko albo nic
Bezpieczeństwo to nie tylko injection — to też integralność przy wielu zapisach. Przelew to odjęcie z jednego konta i dodanie do drugiego; jeśli drugi krok padnie, pierwszy nie może zostać. Służą do tego transakcje (ACID — atomowość, spójność, izolacja, trwałość):
BEGIN TRANSACTION;
UPDATE Konta SET Saldo = Saldo - 100 WHERE Id = 1;
UPDATE Konta SET Saldo = Saldo + 100 WHERE Id = 2;
COMMIT; -- oba naraz; przy błędzie ROLLBACK cofa całośćW EF Core SaveChanges() jest domyślnie transakcyjny — wszystkie zmiany z jednego wywołania zapisują się atomowo albo wcale. Jawną transakcję obejmującą wiele operacji tworzysz przez db.Database.BeginTransaction().
Pułapki rekrutacyjne
Pułapka #1 — FromSqlRaw z interpolacją. EF Core jest bezpieczny, dopóki nie zejdziesz do surowego SQL sklejanego stringiem:
// ❌ podatne — interpolacja wkleja wartość do stringa
db.Users.FromSqlRaw($"SELECT * FROM Users WHERE Login = '{login}'");
// ✅ bezpieczne — FromSqlInterpolated zamienia {login} na parametr
db.Users.FromSqlInterpolated($"SELECT * FROM Users WHERE Login = {login}");Te dwie linie wyglądają niemal identycznie, a różnią się wszystkim. FromSqlRaw z $"..." to fałszywe poczucie bezpieczeństwa — ORM nie chroni, jeśli sam skleisz string.
Pułapka #2 — „ORM załatwia bezpieczeństwo”. Nieprawda w ogólności. LINQ owszem, ale każda ścieżka raw SQL (FromSqlRaw, ExecuteSqlRaw, SqlCommand ze sklejeniem) omija tę ochronę. Bezpieczeństwo daje parametryzacja, nie samo użycie ORM.
Pułapka #3 — AddWithValue i typy. AddWithValue bywa wygodne, ale pozwala bazie zgadywać typ parametru, co czasem psuje użycie indeksu (niejawna konwersja) i wydajność. W kodzie produkcyjnym lepiej podać typ jawnie (Add("@login", SqlDbType.NVarChar)). Bezpieczeństwo to nie osłabia — to kwestia wydajności.
Pułapka #4 — brak transakcji przy wielu zapisach. Dwa osobne SaveChanges() (albo dwa UPDATE) bez wspólnej transakcji mogą zostawić dane w stanie połowicznym, gdy drugi padnie. Operacje, które muszą się udać razem, obejmij jedną transakcją.
Bezpieczne vs niebezpieczne — ściąga
| Podejście | Bezpieczne? |
|---|---|
Sklejanie stringów ("... '" + x + "'") | ❌ nigdy |
$"... {x}" w FromSqlRaw / ExecuteSqlRaw | ❌ podatne |
SqlCommand + Parameters | ✅ tak |
| Dapper z obiektem parametrów | ✅ tak |
| LINQ w EF Core | ✅ tak |
FromSqlInterpolated | ✅ tak |
Podsumowanie
SQL injection bierze się z jednej rzeczy — sklejania danych użytkownika z tekstem zapytania — i znika, gdy zamiast tego użyjesz parametrów: baza traktuje wtedy wartości wyłącznie jako dane, nigdy jako kod. W .NET masz to za darmo w LINQ (EF Core), w Dapperze (obiekt parametrów) i w ADO.NET (Parameters); jedyne miejsca podatne to ręcznie sklejany raw SQL, w tym zdradliwe FromSqlRaw($"..."). Do tego dochodzi integralność: operacje, które muszą się udać razem, obejmujesz transakcją, a SaveChanges() w EF Core jest transakcyjny domyślnie. Parametryzuj zawsze — to jednocześnie ochrona przed atakiem i szybszy, cache’owany plan zapytania.
Co dalej
Masz komplet fundamentów SQL dla .NET developera: zapytania, JOIN-y, indeksy i bezpieczeństwo. Teraz zobacz, jak Entity Framework Core generuje ten SQL za Ciebie (i jak podejrzeć, co naprawdę wysyła), oraz jak Dapper pozwala pisać go ręcznie — już z parametrami, bez ryzyka injection.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →