Основы веб-безопасности
Как веб-приложение на ASP.NET Core узнаёт пользователя и решает, что ему можно, какие уязвимости встречаются чаще всего и какими приёмами и настройками их закрывают. С этих вопросов обычно начинают разговор о безопасности, прежде чем перейти к OAuth, JWT и корпоративному входу.
Перед этой темой: Хостинг и конвейер.
Чем аутентификация отличается от авторизации и как приложение «помнит» пользователя: сессия на cookie или токен?
Заголовок раздела «Чем аутентификация отличается от авторизации и как приложение «помнит» пользователя: сессия на cookie или токен?»Что это
- Аутентификация — проверка, кто пользователь: пароль, сертификат, учётная запись Windows.
- Авторизация — проверка, что ему разрешено: роль, доступ к конкретному документу.
Как приложение помнит пользователя
HTTP не хранит состояние между запросами, поэтому после входа сервер выдаёт «пропуск»:
- cookie — браузер сам прикладывает её к каждому запросу на этот сайт;
- токен — клиент сам добавляет его в заголовок
Authorization: Bearer ....
Как пользоваться
- Зарегистрируйте схему:
AddAuthentication().AddCookie(). - После проверки пароля вызовите
HttpContext.SignInAsync— он выдаст cookie. - Закройте endpoint атрибутом
[Authorize]или вызовомRequireAuthorization().
Частая ошибка
Путать коды ответа. 401 — «не знаю, кто ты, войди», 403 — «знаю, но нельзя».
Как устроено
- Схема — имя, обработчик и его настройки.
AddCookie,AddJwtBearer,AddNegotiateрегистрируют схемы; единственная схема становится схемой по умолчанию. UseAuthenticationвызывает Authenticate у схемы по умолчанию и кладёт результат вHttpContext.User. Анонимный запрос он не отклоняет.UseAuthorizationчитает[Authorize]и политики endpoint’а. Не вошёл — Challenge, вошёл без прав — Forbid.
Как отвечают схемы
| Действие | Cookie | JWT Bearer |
|---|---|---|
| Challenge | 302 на страницу входа | 401 и WWW-Authenticate: Bearer |
| Forbid | 302 на страницу «доступ запрещён» | 403 |
Когда что выбирать
- Cookie — серверный UI (MVC, Razor Pages, Blazor Server) и SPA на том же домене.
- Bearer-токен — API для внешних, мобильных и серверных клиентов.
Ловушки
- С .NET 10 cookie-схема на API-endpoint’ах (
[ApiController], Minimal API с JSON, SignalR) отвечает 401 и 403 без редиректа. Раньшеfetchполучал после редиректа HTML страницы входа. WebApplicationсам добавляетUseAuthenticationиUseAuthorizationсразу послеUseRouting. СUseCorsих вызывают явно, после него.- Сама аутентификация ничего не закрывает: без
[Authorize]или общей политики endpoint открыт.
Под капотом
- Cookie-схема сериализует
AuthenticationTicket— claims и свойства, — шифрует его через Data Protection и пишет в cookie.AspNetCore.Cookies. Сервер ничего не хранит. - По умолчанию cookie
HttpOnly,SameSite=Laxи живёт 14 дней со скользящим продлением. Большая cookie делится на части, но всё равно уходит в каждом запросе. - Узлы фермы должны делить ключи Data Protection, иначе cookie одного узла не расшифруется на другом.
Компромиссы
| Свойство | Cookie | Bearer-токен |
|---|---|---|
| Кто отправляет | браузер сам | код клиента |
| Главный риск | CSRF | кража скриптом на странице |
| Отзыв до истечения | проверка при запросе или ITicketStore |
короткий срок жизни и refresh-токен |
- Заблокированный пользователь работает, пока жива его cookie. Identity закрывает это проверкой метки безопасности (
SecurityStampValidator, по умолчанию раз в 30 минут), своя схема — событиемOnValidatePrincipal. ITicketStoreпереносит билет на сервер: cookie короткая, отзыв мгновенный, но появляются состояние и поход в хранилище на каждый запрос.- SPA без токенов в браузере — паттерн BFF, подробно — в теме OAuth 2.0 и OpenID Connect.
Как проверить в проде
- Метрики .NET 10
aspnetcore.authentication.challenges,aspnetcore.authentication.forbidsиaspnetcore.authorization.attemptsпоказывают отказы по схемам и политикам. - Массовый выход пользователей после деплоя — признак того, что ключи Data Protection у узлов разные.
Итог: cookie и токен — пропуск без серверного состояния, и мгновенный отзыв всегда стоит либо состояния, либо короткого срока жизни.
using System.Security.Claims;using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
// Единственная схема становится схемой по умолчаниюbuilder.Services.AddAuthentication().AddCookie();builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication(); // заполняет HttpContext.Userapp.UseAuthorization(); // проверяет требования endpoint'а
app.MapPost("/login", async (HttpContext ctx) =>{ // Пароль уже проверен: выдаём cookie с зашифрованным билетом var identity = new ClaimsIdentity( [new Claim(ClaimTypes.Name, "alice")], CookieAuthenticationDefaults.AuthenticationScheme); await ctx.SignInAsync(new ClaimsPrincipal(identity));});
app.MapGet("/me", (ClaimsPrincipal user) => new { name = user.Identity!.Name }) .RequireAuthorization(); // без cookie — 401, а не редирект (.NET 10)app.MapGet("/admin", () => new { secret = 42 }) .RequireAuthorization(p => p.RequireRole("Admin")); // alice без роли Admin — 403
app.Run();- Что получит анонимный пользователь на защищённом endpoint’е? — Решает схема: JWT — 401 с
WWW-Authenticate, cookie — редирект на вход, а на API-endpoint’ах с .NET 10 — 401. - Где хранится сессия при cookie-аутентификации? — Нигде: зашифрованный билет лежит в самой cookie; серверное хранение — через
ITicketStore. - Как разлогинить пользователя на всех устройствах? — Сменить метку безопасности, которую проверяет
SecurityStampValidator, или хранить билеты на сервере. - Чем
AddSessionотличается от cookie-аутентификации? — Сессия хранит данные на сервере по идентификатору в cookie и не подтверждает личность. - Зачем несколько схем в одном приложении? — Например, cookie для UI и JWT для API; политика или
[Authorize(AuthenticationSchemes = ...)]выбирает нужную.
Что такое OWASP Top 10 и какие уязвимости в нём главные?
Заголовок раздела «Что такое OWASP Top 10 и какие уязвимости в нём главные?»Что это
OWASP (Open Worldwide Application Security Project) — открытое сообщество по безопасности приложений. Top 10 — его рейтинг главных рисков веб-приложений, который обновляется раз в несколько лет: 2017, 2021, 2025.
Первая пятёрка 2025 года
- A01 Broken Access Control — пользователь получает чужие данные или функции.
- A02 Security Misconfiguration — опасные настройки: подробные ошибки в проде, открытые служебные endpoint’ы.
- A03 Software Supply Chain Failures — уязвимые или подменённые пакеты и сборка.
- A04 Cryptographic Failures — данные без шифрования, слабые алгоритмы.
- A05 Injection — SQL-инъекции, XSS, командные инъекции.
Частая ошибка
Считать, что фреймворк защищает от всего. ORM закрывает SQL-инъекции, но не проверит, что заказ принадлежит текущему пользователю.
Как устроено
Категория — группа слабостей из каталога CWE. Место зависит от частоты и тяжести в данных тестирования, а две категории добавляет опрос практиков.
Риск — пример — защита в .NET
| Риск | Пример | Защита в .NET |
|---|---|---|
| A01 Контроль доступа | GET /orders/43 отдаёт чужой заказ |
проверка владельца, [Authorize], FallbackPolicy |
| A02 Конфигурация | страница исключений разработчика в проде | UseExceptionHandler, проверка окружения |
| A03 Цепочка поставок | пакет с известной CVE | NuGetAudit, lock-файл, Package Source Mapping |
| A04 Криптография | пароль в SHA-256, сайт по HTTP | PasswordHasher, HTTPS и HSTS, Data Protection |
| A05 Инъекции | SQL из конкатенации строк | параметры, FromSql, кодирование Razor |
| A06 Небезопасный дизайн | сброс пароля без лимита попыток | моделирование угроз, rate limiting |
| A07 Аутентификация | перебор паролей | блокировка в Identity, MFA |
| A08 Целостность | BinaryFormatter, неподписанные обновления |
System.Text.Json, подпись артефактов |
| A09 Журналирование | неудачный вход не попадает в лог | события безопасности в логах и алерты |
| A10 Исключительные ситуации | сбой проверки прав пропускает запрос | отказ при сбое (fail closed), ответ без деталей |
Ловушки
- В редакции 2021 года SSRF был отдельной A10, а уязвимые компоненты — A06. Номера в старых отчётах не совпадают с 2025.
[Authorize]проверяет, что пользователь вошёл, но не что ресурс его. A01 закрывается проверкой в коде.
Под капотом
- 2025 — восьмая редакция. Восемь категорий выбраны по данным, две — опросом, чтобы учесть риски, которые данные ещё не видят.
- Категории стали шире и ближе к первопричине. Цепочка поставок охватывает не только известные CVE, но и CI/CD, а A10 — отказ «в открытую сторону» и утечку деталей ошибок.
Компромиссы
- Top 10 — документ осведомлённости. Для требований и приёмки OWASP предлагает ASVS, для тестирования — WSTG.
- Инъекции опустились в рейтинге во многом благодаря ORM и шаблонизаторам. Остаётся то, чего фреймворк знать не может: владелец ресурса и допустимые действия.
- С .NET 10
NuGetAuditпо умолчанию проверяет и транзитивные пакеты. Новые предупреждения могут ломать сборку сTreatWarningsAsErrors— их разбирают, а не глушат. FallbackPolicyсRequireAuthenticatedUser()закрывает всё по умолчанию: забытый[Authorize]перестаёт быть дырой, а открытые endpoint’ы помечают[AllowAnonymous]явно.
Как проверить в проде
- В CI — анализаторы безопасности Roslyn (
CA2100,CA5350,CA5351) иdotnet list package --vulnerable --include-transitive; на стенде — DAST, например ZAP. - Всплески 403 и 404 на переборе идентификаторов в логах — признак поиска IDOR.
Итог: Top 10 задаёт приоритеты, а не полноту: фреймворк закрывает инъекции, а доступ, конфигурация и зависимости остаются процессом команды.
using System.Security.Claims;using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddCookie();// Всё закрыто по умолчанию: открытые endpoint'ы помечают AllowAnonymousbuilder.Services.AddAuthorizationBuilder() .SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());builder.Services.AddSingleton<OrderStore>();
var app = builder.Build();
app.MapGet("/orders/{id:int}", (int id, ClaimsPrincipal user, OrderStore store) =>{ var order = store.Find(id); // A01: вход проверила политика, владельца проверяем сами return order is not null && order.OwnerId == user.Identity?.Name ? Results.Ok(order) : Results.NotFound(); // 404, а не 403: не подтверждаем, что чужой заказ есть});
app.Run();
public record Order(int Id, string OwnerId, decimal Total);
public class OrderStore{ private readonly Dictionary<int, Order> _orders = new() { [42] = new(42, "alice", 100m) }; public Order? Find(int id) => _orders.GetValueOrDefault(id);}- Что изменилось в редакции 2025 года по сравнению с 2021? — SSRF вошёл в A01, появились цепочка поставок (A03) и обработка исключительных ситуаций (A10), конфигурация поднялась на второе место.
- Почему XSS относится к инъекциям? — Данные пользователя попадают в HTML или JavaScript и становятся кодом; с 2021 года XSS входит в Injection.
- Что такое IDOR и как его найти? — Доступ к чужому объекту подменой id; ищут тестами «пользователь A запрашивает ресурс пользователя B».
- Чем Top 10 отличается от ASVS? — Top 10 — рейтинг рисков для осведомлённости, ASVS — проверяемый список требований по уровням.
- Как в .NET следить за уязвимыми пакетами? —
NuGetAuditпри restore иdotnet list package --vulnerable --include-transitive.
Как работают XSS и CORS и как от них защищаться?
Заголовок раздела «Как работают XSS и CORS и как от них защищаться?»Что это
- XSS (межсайтовый скриптинг) — злоумышленник оставляет в комментарии
<script>...</script>, а сайт показывает комментарий без кодирования. Скрипт выполняется у каждого читателя и действует от его имени. - CORS — разрешение от сервера. Скрипт с
https://shop.exampleпрочитает ответhttps://api.example, только если API вернёт подходящийAccess-Control-Allow-Origin.
Как пользоваться
- Выводите данные в Razor через
@: он превращает<в<.Html.Rawс данными пользователя не используйте. - Ставьте cookie флаги
HttpOnlyиSecure. - В API разрешайте CORS только своим фронтендам:
policy.WithOrigins("https://shop.example").
Частая ошибка
Считать CORS защитой API. Он ограничивает только браузер, а curl или сервер злоумышленника вызывают API без всякого CORS.
Как устроено: XSS
| Вид | Откуда берётся скрипт |
|---|---|
| Хранимый | из базы: комментарий, имя в профиле |
| Отражённый | из запроса: параметр ?q= попадает в ответ |
| DOM-based | JavaScript страницы сам вставляет данные через innerHTML |
Кодирование зависит от контекста: HTML — HtmlEncoder, JavaScript — JavaScriptEncoder, URL — UrlEncoder. Данные для скрипта надёжнее положить в атрибут data-* и прочитать через dataset.
Как устроено: CORS
- Браузер добавляет к межсайтовому запросу заголовок
Origin. - Простой запрос —
GET,HEADилиPOSTс формой илиtext/plain— уходит сразу. Браузер лишь решает, отдать ли ответ скрипту. - Остальные —
PUT,DELETE,application/json, свои заголовки — сначала идут preflight-запросомOPTIONS. Без разрешения основной запрос не отправляется.
Ловушки
AllowAnyOrigin()вместе сAllowCredentials()ASP.NET Core запрещает исключением.SetIsOriginAllowed(_ => true)этот запрет обходит и открывает API с cookie любому сайту.UseCorsставят послеUseRoutingи до аутентификации и авторизации: preflight приходит без учётных данных.- CORS не мешает CSRF: форма с чужого сайта — простой запрос. Antiforgery разобран в теме MVC, Razor и SignalR.
Под капотом
- Razor кодирует
@по правилам HTML-атрибута: они строже HTML и подходят обоим контекстам. Кодировщики по умолчанию пропускают только Basic Latin, поэтому кириллица в разметке становится&#x...;, пока не расширитьHtmlEncoderчерезUnicodeRanges. HtmlString,Html.RawиMarkupStringв Blazor отключают кодирование. Пользовательский HTML пропускают через санитайзер, например HtmlSanitizer, или принимают Markdown без HTML.
Эшелонированная защита
- CSP.
script-src 'nonce-...' 'strict-dynamic',object-src 'none'иbase-uri 'none'не дают выполниться внедрённому inline-скрипту. Встроенного middleware для CSP нет: заголовок ставят своим middleware или пакетом NetEscapades.AspNetCore.SecurityHeaders. Сначала включаютContent-Security-Policy-Report-Only, чтобы собрать нарушения. - Cookie.
HttpOnlyскрывает cookie отdocument.cookie,Secureне пускает её в HTTP. Префикс__Host-требуетSecureиPath=/и запрещаетDomain, поэтому поддомен её не перезапишет.
Компромиссы
- Токен в
localStorageудобен SPA, но при XSS сразу утекает. Cookie сHttpOnlyскрипт не прочитает, но отправит запросы от имени пользователя, пока открыта страница. SetPreflightMaxAgeкеширует preflight и убирает лишний round trip. Браузеры ограничивают срок сверху: Chromium — двумя часами.
Как проверить в проде
- Отчёты CSP показывают и попытки атак, и сломанные легитимные скрипты.
- DAST-сканер проверяет CSP, флаги cookie и ответ API на чужой
Origin.
Итог: кодирование — основная защита от XSS, CSP и флаги cookie ограничивают ущерб, а CORS настраивают точным списком источников.
using Microsoft.AspNetCore.CookiePolicy;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => options.AddPolicy("spa", policy => policy .WithOrigins("https://shop.example") // точный список, не "*" .WithMethods("GET", "POST") .WithHeaders("Content-Type") .AllowCredentials() // вместе с AllowAnyOrigin() — InvalidOperationException .SetPreflightMaxAge(TimeSpan.FromMinutes(10))));
var app = builder.Build();
app.Use(async (context, next) =>{ // Скрипты только со своего origin, inline-скрипты запрещены context.Response.Headers.ContentSecurityPolicy = "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"; await next();});app.UseCookiePolicy(new CookiePolicyOptions{ HttpOnly = HttpOnlyPolicy.Always, // document.cookie их не увидит Secure = CookieSecurePolicy.Always, // только по HTTPS MinimumSameSitePolicy = SameSiteMode.Lax});app.UseRouting();app.UseCors();
app.MapPost("/api/cart", () => new { items = 3 }).RequireCors("spa");
app.Run();OPTIONS /api/cart HTTP/1.1Origin: https://shop.exampleAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: content-type
HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://shop.exampleAccess-Control-Allow-Credentials: trueAccess-Control-Allow-Methods: GET,POSTAccess-Control-Allow-Headers: Content-TypeAccess-Control-Max-Age: 600Если Origin не из списка, сервер тоже ответит 204, но без заголовков Access-Control-Allow-*, и браузер не отправит основной запрос.
- Чем хранимый XSS отличается от DOM-based? — Хранимый приходит с сервера из базы, DOM-based создаёт сам JavaScript страницы через
innerHTMLилиeval; серверное кодирование от второго не спасает. - Защищает ли
HttpOnlyот XSS? — Нет, он только прячет cookie от скрипта; запросы от имени пользователя скрипт всё равно отправит. - Когда браузер шлёт preflight? — Для методов кроме
GET,HEADиPOST, дляContent-Typeвродеapplication/jsonи для своих заголовков. - Почему нельзя
*вместе с credentials? — Иначе любой сайт читал бы ответы с cookie пользователя; спецификация и ASP.NET Core это запрещают. - Защищает ли CORS от CSRF? — Нет, простой межсайтовый POST выполняется; нужны antiforgery-токены и
SameSite.
Как защититься от инъекций и как правильно хранить пароли и секреты?
Заголовок раздела «Как защититься от инъекций и как правильно хранить пароли и секреты?»Что это
- SQL-инъекция. Запрос собран конкатенацией, а пользователь вводит в поле логина
' OR 1=1 --. Условие становится всегда истинным, проверка пароля пропускается. - Хеш пароля — необратимое преобразование. При входе хешируют введённый пароль и сравнивают хеши.
Как пользоваться
- В EF Core пишите LINQ или
FromSql($"... {name}"): интерполяция превращается в параметр. - В Dapper передавайте
new { name }, в ADO.NET —SqlParameter. - Пароли храните через ASP.NET Core Identity или
PasswordHasher<TUser>. - Строки подключения и ключи API — в User Secrets локально и в хранилище секретов на сервере, подробно — в теме Конфигурация и логирование.
Частая ошибка
Хранить SHA256(password). Хеш быстрый и без соли: одинаковые пароли дают одинаковые хеши, а словарь перебирается за минуты.
Как устроено: инъекции
| Где | Опасно | Безопасно |
|---|---|---|
| EF Core | FromSqlRaw("..." + name) |
LINQ, FromSql($"...{name}") |
| Dapper, ADO.NET | конкатенация в тексте команды | параметры @name |
| Процессы | cmd.exe /c со строкой от пользователя |
ArgumentList без оболочки |
| Файлы | Path.Combine(root, userPath) |
Path.GetFullPath и проверка, что путь внутри root |
Командная инъекция — тот же приём для оболочки: & или ; в данных добавляют свою команду. Имена таблиц и колонок параметром не передать — их выбирают из белого списка.
Как устроено: пароли
- Для каждого пользователя генерируется случайная соль. Одинаковые пароли дают разные хеши, и заранее посчитанные таблицы бесполезны.
- Медленная функция (KDF) прогоняет пароль с солью десятки и сотни тысяч раз.
- В базу пишут алгоритм, параметры, соль и хеш одной строкой.
- При входе хеш пересчитывается с сохранёнными параметрами и сравнивается за постоянное время.
Ловушки
Path.Combineотбрасываетroot, если пользователь передал абсолютный путь.- Шифровать пароли вместо хеширования: утечёт ключ — утекут все пароли.
Под капотом
PasswordHasherформата V3 с .NET 7: PBKDF2-HMAC-SHA512, соль 128 бит, хеш 256 бит, 100 000 итераций. Число итераций лежит в самом хеше, поэтомуPasswordHasherOptions.IterationCountможно поднять: старые хеши даютSuccessRehashNeeded, иUserManagerпересчитывает их при входе.- OWASP советует Argon2id (19 МиБ памяти, 2 итерации), scrypt, bcrypt (cost от 10) или PBKDF2-HMAC-SHA512 с 220 000 итераций. Argon2 и bcrypt в .NET — сторонние пакеты; bcrypt учитывает только первые 72 байта пароля.
- Для своего PBKDF2 в .NET 10 остаётся статический
Rfc2898DeriveBytes.Pbkdf2: конструкторы класса устарели (SYSLIB0060).
Компромиссы
- Медленный хеш — намеренная нагрузка на CPU при каждом входе. Без rate limiting и блокировки учётных записей форма входа становится точкой DoS.
- Небезопасная десериализация:
BinaryFormatterв .NET 9+ всегда бросаетPlatformNotSupportedException, а Newtonsoft.Json сTypeNameHandling.AutoилиAllдаёт атакующему выбрать тип.System.Text.Jsonсоздаёт только объявленные типы, полиморфизм — по белому списку[JsonDerivedType]. - Data Protection меняет ключ каждые 90 дней и хранит старые для расшифровки. Удалённый ключ — потерянные данные, а для бессрочного хранения Microsoft советует средства со строгим управлением ключами.
Как проверить в проде
- Хеши Identity с префиксом
AQAAAAEAACcQ— старый формат (SHA-256, 10 000 итераций),AQAAAAIAAYag— текущий. Доля старых показывает, сколько пользователей не входили после обновления. - Анализаторы
CA2100,CA3001,CA3006иCA2326в CI ловят SQL из строк, инъекции и опасныйTypeNameHandling.
Итог: инъекции закрывает структурное разделение кода и данных, а хеш пароля несёт свои параметры, которые повышают со временем.
using Microsoft.AspNetCore.DataProtection;using Microsoft.AspNetCore.Identity;using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);builder.Services.AddDataProtection().SetApplicationName("shop"); // одно имя на всех узлах фермыvar app = builder.Build();
// По умолчанию 100 000 итераций; OWASP для PBKDF2-HMAC-SHA512 советует 220 000var hasher = new PasswordHasher<User>( Options.Create(new PasswordHasherOptions { IterationCount = 220_000 }));var user = new User("alice");var hash = hasher.HashPassword(user, "P@ssw0rd"); // AQAAAAIAA1tg...: формат, итерации, соль, хешConsole.WriteLine(hasher.VerifyHashedPassword(user, hash, "P@ssw0rd")); // SuccessConsole.WriteLine(hasher.VerifyHashedPassword(user, hash, "wrong")); // Failed
// Data Protection: обратимое шифрование, ключами управляет фреймворкvar protector = app.Services.GetRequiredService<IDataProtectionProvider>() .CreateProtector("Shop.PassportNumber.v1"); // purpose изолирует данные разных задачvar stored = protector.Protect("AB123456");Console.WriteLine(protector.Unprotect(stored)); // AB123456
app.Run();
public record User(string Name);// NuGet: Dapper, Microsoft.Data.SqlClientusing System.Diagnostics;using System.Net;using Dapper;using Microsoft.Data.SqlClient;
var builder = WebApplication.CreateBuilder(args);var app = builder.Build();
app.MapGet("/users", async (string name, IConfiguration config) =>{ await using var db = new SqlConnection(config.GetConnectionString("Main")); // Опасно: "... WHERE Name = '" + name + "'" — при name = "' OR 1=1 --" вернёт всех // Безопасно: значение уходит параметром @name и не становится частью SQL return await db.QueryAsync<UserRow>("SELECT Id, Name FROM Users WHERE Name = @name", new { name });});
app.MapPost("/ping", (string host) =>{ // Белый список формата: только IP-адрес, никаких "-n 1000" и "& del *" if (!IPAddress.TryParse(host, out _)) return Results.BadRequest(); // Без оболочки: host уходит одним аргументом, & и ; ничего не значат var psi = new ProcessStartInfo("ping") { UseShellExecute = false }; psi.ArgumentList.Add(host); using var process = Process.Start(psi); return Results.Accepted();});
app.Run();
public record UserRow(int Id, string Name);- Защищает ли EF Core от SQL-инъекций полностью? — LINQ и
FromSqlс интерполяцией — да;FromSqlRawиExecuteSqlRawс конкатенацией — нет. - Почему не SHA-256 с солью? — Соль убивает заранее посчитанные таблицы, но не скорость: видеокарта перебирает миллиарды SHA-256 в секунду.
- Как перейти на более стойкий хеш без сброса паролей? — Хранить версию и параметры в хеше и пересчитывать при успешном входе, как
SuccessRehashNeededв Identity. - Что такое перец (pepper)? — Общий секрет вне базы, который добавляют к паролю; утечка одной базы без него не даёт перебора.
- Чем Data Protection отличается от хеширования? — Шифрование обратимо и нужно для данных, которые придётся прочитать; пароль читать не нужно, его хешируют.
Как работают HTTPS и TLS и что настроить в продакшене?
Заголовок раздела «Как работают HTTPS и TLS и что настроить в продакшене?»Что это
- TLS — протокол поверх TCP: шифрует данные, защищает их от подмены и подтверждает подлинность сервера.
- Сертификат — открытый ключ сервера и его доменное имя, подписанные центром сертификации (CA), которому доверяет браузер.
Зачем
Без HTTPS пароль, cookie и токен идут открытым текстом. Их читает и подменяет любой узел по пути, например публичный Wi-Fi.
Как пользоваться
app.UseHttpsRedirection()отвечает на HTTP-запрос редиректом на HTTPS.app.UseHsts()вне Development добавляет заголовокStrict-Transport-Security, и браузер запоминает, что сайт работает только по HTTPS.- Сертификат разработчика ставит
dotnet dev-certs https --trust.
Частая ошибка
Ограничиться редиректом без HSTS. Первый запрос по HTTP перехватывают и не дают перейти на HTTPS — это SSL stripping.
Как устроено: рукопожатие TLS 1.3
- ClientHello: версии, шифры, ключевая доля (key share) для обмена Диффи — Хеллмана и имя сайта (SNI).
- ServerHello: выбранный шифр и ключевая доля сервера. Обе стороны вычисляют общий секрет, дальше всё зашифровано.
- Сервер отправляет сертификат с промежуточными, подпись рукопожатия закрытым ключом (CertificateVerify) и Finished.
- Клиент проверяет цепочку и подпись, отвечает Finished и сразу шлёт HTTP-запрос. Один round trip вместо двух у TLS 1.2.
Цепочка доверия
Сертификат сайта подписан промежуточным CA, тот — корневым из хранилища ОС или браузера. Клиент проверяет подписи до корня, срок действия и совпадение хоста с именами в SAN.
Ловушки
- За прокси с терминацией TLS приложение видит
http. Без forwarded headersUseHttpsRedirectionзацикливается, аUseHstsмолчит: заголовок отправляется только в HTTPS-ответах. - Не найден HTTPS-порт — в логе «Failed to determine the https port for redirect», редиректа нет. Порт задают
HttpsPortилиASPNETCORE_HTTPS_PORT. - В IIS TLS расшифровывает сам IIS, и приложение видит
httpsбез настройки. Редирект и HSTS можно включить и в IIS (с IIS 10 версии 1709) — тогда их не дублируют в коде.
Под капотом
- В TLS 1.3 обмен ключами только эфемерный — (EC)DHE, а ключ сертификата лишь подписывает рукопожатие. Украденный закрытый ключ не расшифрует записанный ранее трафик.
- Kestrel шифрует через
SslStream, а тот — через SChannel на Windows и OpenSSL на Linux. TLS 1.3 на Windows — начиная с Windows Server 2022.
Компромиссы
| Где расшифровывать | Плюс | Минус |
|---|---|---|
| На прокси | сертификаты в одном месте, CPU приложения свободен | внутри сети трафик открыт |
| На прокси и снова до Kestrel | шифрование на всём пути | второй сертификат и рукопожатие |
| Только в Kestrel (L4-прокси) | прокси не видит трафик | нет WAF и маршрутизации по HTTP на прокси |
- HSTS preload.
max-ageот года,includeSubDomainsиpreloadпозволяют попасть в список, вшитый в браузеры, и защитить даже первый визит. Удаление из списка занимает месяцы, а поддомен без HTTPS станет недоступен. - mTLS.
ClientCertificateMode.RequireCertificateв Kestrel иAddCertificate()из пакета Microsoft.AspNetCore.Authentication.Certificate. За прокси сертификат передают заголовком и читают черезUseCertificateForwarding. - Срок сертификатов. С 15 марта 2026 года публичные сертификаты выдают не больше чем на 200 дней, с 2029-го — на 47. Ротация должна быть автоматической: ACME, Key Vault.
Как проверить в проде
kestrel.tls_handshake.durationс атрибутомtls.protocol.versionпоказывает цену рукопожатий и долю TLS 1.2.openssl s_client -connect host:443 -showcertsпоказывает, отдаёт ли сервер промежуточные сертификаты.
Итог: TLS в проде — это решения инфраструктуры: где расшифровывать, как ротировать сертификаты и насколько необратимо включать HSTS.
using Microsoft.AspNetCore.Connections.Features;using Microsoft.AspNetCore.Server.Kestrel.Https;
var builder = WebApplication.CreateBuilder(args);
// Сертификат — в конфигурации (Kestrel:Certificates:Default), протоколы не задаём: решает ОСbuilder.WebHost.ConfigureKestrel(kestrel => kestrel.ConfigureHttpsDefaults(https => https.ClientCertificateMode = ClientCertificateMode.AllowCertificate)); // mTLS по желанию клиента
builder.Services.AddHsts(options =>{ options.MaxAge = TimeSpan.FromDays(365); // по умолчанию 30 дней options.IncludeSubDomains = true; options.Preload = true; // только когда все поддомены на HTTPS});builder.Services.AddHttpsRedirection(options =>{ options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect; // по умолчанию 307 options.HttpsPort = 443;});
var app = builder.Build();
if (!app.Environment.IsDevelopment()) app.UseHsts(); // Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadapp.UseHttpsRedirection();
app.MapGet("/", (HttpContext ctx) => ctx.Features.Get<ITlsHandshakeFeature>()?.Protocol.ToString()); // "Tls13"
app.Run();{ "Kestrel": { "Certificates": { "Default": { "Path": "/certs/shop.crt", "KeyPath": "/certs/shop.key" } } }}- Чем TLS 1.3 быстрее TLS 1.2? — Полное рукопожатие за один round trip вместо двух; при возобновлении возможен 0-RTT, но такие данные можно повторить.
- Что такое forward secrecy? — Ключи сеанса из одноразового обмена Диффи — Хеллмана: кража ключа сертификата не раскрывает прошлый трафик.
- Почему HSTS не защищает первый визит? — Браузер ещё не видел заголовок; закрывает это только preload-список.
- Почему API не стоит редиректить на HTTPS? — Клиент уже отправил данные и токен по HTTP; API лучше не слушать на HTTP или отвечать 400.
- Как сделать mTLS между сервисами? — Kestrel с
ClientCertificateMode.RequireCertificateиAddCertificate()или service mesh, который делает mTLS за приложение.