Лучшие Практики Безопасности REST API в ASP.NET Core. Продолжение
Начало
4. Авторизация с политиками
Аутентификация определяет, кто является пользователем. Авторизация определяет, что ему разрешено делать. Простая проверка через
[Authorize] проверяет только то, что пользователь авторизован. Для реальных правил — «только менеджеры могут удалять заказы», «пользователи могут видеть только свои данные» — необходима авторизация на основе политик и ресурсов. Определите политику:builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("orders:write", p =>
p.RequireRole("Manager")
.RequireClaim("permission", "orders:write"));
});
Примените её к методу действия контроллера:
public class OrdersController : ControllerBase
{
// …
[HttpDelete("{id:int}")]
[Authorize(Policy = "orders:write")]
public IActionResult Delete(int id)
{
_orderService.Delete(id);
return NoContent();
}
}
Или к конечной точке минимальных API:
app.MapDelete("/api/orders/{id:int}",
(int id, IOrderService orders) =>
{
orders.Delete(id);
return Results.NoContent();
})
.RequireAuthorization("orders:write");Когда правило зависит от конкретного ресурса — например, «только владелец может редактировать этот заказ» — используйте авторизацию на основе ресурсов с помощью
IAuthorizationService.AuthorizeAsync(user, order, "OrderOwner"), которая оценивает политику на основе фактической сущности. Политики позволяют хранить логику авторизации в одном месте, вне тела конечных точек.5. Принцип наименьших привилегий
Предоставьте каждому клиенту минимальный необходимый доступ и ничего больше. Токен, набор утверждений или ключ API должны предоставлять только те разрешения, которые необходимы для выполнения его задачи. Мобильное приложение, которое только читает каталог товаров, не должно иметь токен, позволяющий удалять заказы. Ограничьте это на уровне токена. Когда ваш поставщик идентификации выдаёт токен, он должен включать только те области действия, которые были предоставлены клиенту, а ваши политики - проверять их наличие:
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("catalog:read", p =>
p.RequireClaim("scope", "catalog:read"));
opts.AddPolicy("orders:write", p =>
p.RequireClaim("scope", "orders:write"));
});
Тот же принцип применим к ключам API и аккаунтам баз данных — ограничьте их область действия. Если учётные данные утекут, принцип наименьших привилегий ограничит радиус атаки только тем, что могут сделать эти аккаунты.
6. Проверка и очистка входных данных
Некорректные или вредоносные входные данные должны отклоняться на границе, до того, как они достигнут бизнес-логики или базы данных. ASP.NET Core помогает в этом: атрибут
[ApiController] автоматически возвращает ошибку 400 с подробным описанием проблемы на запрос, не прошедший валидацию модели. Добавьте аннотации данных или, для более сложных правил, FluentValidation:public class CreateProductRequest
{
[Required]
[StringLength(200, MinimumLength = 1)]
public string Name { get; set; } = string.Empty;
[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
}
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult
Create(CreateProductRequest req)
{
// Если мы здесь, модель прошла валидацию
// …
}
}
В минимальных API проверка выполняется с помощью фильтра или явно:
app.MapPost("/api/products",
(CreateProductRequest req,
IValidator<CreateProductRequest> validator) =>
{
var result = validator.Validate(request);
if (!result.IsValid)
return Results
.ValidationProblem(result.ToDictionary());
// …
});Ранняя проверка предотвращает целый класс атак — слишком длинные строки, числа, выходящие за пределы допустимого диапазона, отсутствующие поля и т.п.
7. Овер-постинг
Никогда не привязывайте входящие JSON-данные напрямую к сущности вашей БД. Так клиенты смогут устанавливать поля, которые они никогда не должны контролировать —
IsAdmin, Balance, Status или ID другого пользователя. Это называется овер-постингом или массовым присваиванием.Используйте отдельные DTO запроса, который отображает только те поля, которые клиенту разрешено устанавливать, а затем самостоятельно сопоставляйте его с сущностью:
// DTO запроса – то, что клиент может изменять
public record UpdateProductRequest(string Name, decimal Price);
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(
int id, UpdateProductRequest request)
{
var prod = await _dbContext.Products.FindAsync(id);
if (prod is null)
return NotFound();
// Задаём только разрешённые поля
prod.Name = request.Name;
prod.Price = request.Price;
await _dbContext.SaveChangesAsync();
return NoContent();
}
Сущность Product также может содержать поля
CreatedAt, OwnerId или IsFeatured, но, т.к. клиент может отправлять только Name и Price, эти поля недоступны извне. Отдельные DTO запросов требуют небольшого количества дополнительного кода, но закрывают целую категорию ошибок, приводящих к повышению привилегий.Продолжение следует…
Источник: https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore