SQL Injection, XSS, CSRF — как предотвращать в ASP.NET Core?
SQL Injection
var sql = $"SELECT * FROM users WHERE email = '{email}' AND password_hash = '{hash}'";
var user = await db.Users.FromSqlRaw(sql).FirstOrDefaultAsync(); // интерполяция ДО FromSqlRaw — строка уже склеена
using var cmd = new NpgsqlCommand("SELECT * FROM orders WHERE id = " + id, conn);// EF Core LINQ — всегда параметризован
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == email);
// Raw SQL с интерполяцией FormattableString — значения становятся параметрами
var orders = await db.Orders.FromSql($"SELECT * FROM orders WHERE customer_id = {customerId}").ToListAsync();
// ADO.NET / Dapper
using var cmd = new NpgsqlCommand("SELECT * FROM orders WHERE id = @id", conn);
cmd.Parameters.AddWithValue("id", id);
var list = await conn.QueryAsync<Order>("SELECT * FROM orders WHERE status = @status", new { status });
// Динамическая сортировка — белый список, параметризовать имена столбцов нельзя
var column = sortBy switch { "date" => "created_at", "total" => "total", _ => "id" };Дополнительные слои: учётная запись БД приложения без DDL и прав суперпользователя, отдельные read-only учётки для отчётов, WAF.
XSS
| Вид | Где живёт payload | Пример |
|---|---|---|
| Stored | В БД, показывается всем | Отзыв, комментарий, имя профиля |
| Reflected | В запросе, отражается в ответе | /search?q=<script>... в тексте «Результаты по …» |
| DOM-based | Обрабатывается JS на клиенте | element.innerHTML = location.hash |
Защита в ASP.NET Core
<p>@review.Text</p> @* <script> → <script> — безопасно *@
<p>@Html.Raw(review.Text)</p> @* ОПАСНО: вывод без экранирования *@var sanitizer = new HtmlSanitizer(); // пакет HtmlSanitizer (Ganss.Xss)
sanitizer.AllowedTags.Clear();
foreach (var tag in new[] { "p", "b", "i", "ul", "ol", "li", "a", "br" }) sanitizer.AllowedTags.Add(tag);
var safeHtml = sanitizer.Sanitize(userHtml); // удаляет <script>, onerror=, javascript:-ссылки- Кодирование по контексту: HTML-тело, атрибут, JavaScript, URL, CSS — разные правила. Используйте
HtmlEncoder,JavaScriptEncoder,UrlEncoderизSystem.Text.Encodings.Web. - SPA (React, Angular, Blazor) экранируют по умолчанию; опасны
dangerouslySetInnerHTML,[innerHTML],bypassSecurityTrust*,MarkupString. - API, возвращающие JSON с
Content-Type: application/json, сами по себе XSS не создают — важна обработка на клиенте.
Content-Security-Policy
app.Use(async (ctx, next) =>
{
ctx.Response.Headers.ContentSecurityPolicy =
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'";
await next();
});CSP запрещает браузеру выполнять inline-скрипты и скрипты с чужих доменов — даже если XSS-payload попал на страницу, он не выполнится. Для необходимых inline-скриптов — nonce или hash. Плюс HttpOnly-cookie: JavaScript не может прочитать cookie сессии.
CSRF
Antiforgery-токены
<form method="post" asp-action="Transfer">
@* <input name="__RequestVerificationToken" type="hidden" value="..."> генерируется tag helper-ом *@
<input asp-for="Amount" />
<button>Перевести</button>
</form>builder.Services.AddControllersWithViews(o => o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())); // все POST/PUT/DELETE
// Minimal API (.NET 8+): формы проверяются автоматически при UseAntiforgery
builder.Services.AddAntiforgery();
app.UseAntiforgery();
app.MapPost("/transfer", ([FromForm] TransferForm form) => Results.Ok());
// SPA с cookie-аутентификацией: отдать токен в cookie, фронтенд возвращает его заголовком
builder.Services.AddAntiforgery(o => o.HeaderName = "X-XSRF-TOKEN");Токен (synchronizer token): сервер кладёт секрет в cookie и связанный токен в форму/заголовок; злоумышленник с чужого сайта не может прочитать токен (Same-Origin Policy), поэтому не может сформировать валидный запрос.
SameSite cookie
| Значение | Отправка cookie с чужого сайта | Замечание |
|---|---|---|
Strict |
Никогда | Максимальная защита, но переход по ссылке с другого сайта — без сессии |
Lax (по умолчанию в браузерах) |
Только навигационные GET (переход по ссылке) | Защищает от CSRF через POST-формы |
None (+ Secure) |
Всегда | Нужно для сторонних встраиваний (iframe, OAuth-сценарии); без antiforgery — уязвимо |
Когда CSRF не угрожает
API, аутентифицирующее запросы через заголовок Authorization: Bearer ...: браузер не добавляет этот заголовок автоматически, чужой сайт не знает токен. Но если токен хранится в cookie — снова нужна защита.
Сводная таблица
| Атака | Суть | Главная защита в ASP.NET Core |
|---|---|---|
| SQL Injection | Ввод становится SQL-кодом | EF Core LINQ, FromSql с интерполяцией, параметры; белые списки |
| XSS | Ввод становится JS-кодом в браузере | Экранирование Razor, санитизация HTML, CSP, HttpOnly |
| CSRF | Чужой сайт использует cookie жертвы | Antiforgery-токены, SameSite, Bearer в заголовке |
Вопросы на засыпку
Защищает ли ORM полностью от SQL-инъекций?
Только при использовании LINQ и параметризованных API. FromSqlRaw/ExecuteSqlRaw с заранее склеенной строкой, динамические имена столбцов, хранимые процедуры, собирающие SQL внутри (EXEC(@sql)), остаются уязвимыми.
Почему валидация ввода не главная защита от XSS?
Опасность зависит от контекста вывода: одно и то же значение безопасно в HTML-тексте и опасно в атрибуте href или внутри <script>. Правильная защита — кодирование при выводе в конкретный контекст; валидация ввода — дополнительный слой.
Нужна ли CSRF-защита для GET-запросов?
GET по стандарту должен быть безопасным (не изменять состояние). Если GET-эндпоинт что-то меняет (/logout, /delete?id=), он уязвим и к CSRF (через <img src>), и к prefetch. Решение — изменения только через POST/PUT/DELETE.
Как XSS обходит CSRF-защиту?
Скрипт, выполняющийся на вашем же домене, может прочитать antiforgery-токен со страницы и отправить «правильный» запрос. Поэтому XSS обесценивает CSRF-защиту — от XSS нужно защищаться в первую очередь.
Итог
SQL-инъекции предотвращаются параметризацией (EF Core LINQ, FromSql с интерполяцией) и белыми списками, XSS — экранированием вывода, санитизацией HTML и CSP, CSRF — antiforgery-токенами, SameSite-cookie и Bearer-токенами в заголовках. ASP.NET Core делает безопасный вариант вариантом по умолчанию — главное не обходить его через FromSqlRaw со склейкой, Html.Raw и отключение antiforgery.