|
Neste artigo, abordaremos as principais diretrizes e técnicas recomendadas para proteger suas Web APIs desenvolvidas com ASP .NET Core, abordando desde criptografia e autenticação até validação de dados e prevenção de ataques comuns. |

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = true;
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
|
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://seu-idp.com";
options.Audience = "loja-api";
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
|
Authorization: Bearer <token>
a cada requisição. Sua API o valida e lê a identidade do usuário a partir das
alegações (claims) do token — sem necessidade de armazenomento de
sessão, sessões pegajosas (sticky sessions) ou estado no servidor.
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://seu-idp.com",
ValidateAudience = true,
ValidAudience = "loja-api",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
});
|
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("pedidos:write", policy =>
policy.RequireRole("Gerente")
.RequireClaim("permission", "pedidos:write"));
});
|
[ApiController]
[Route("api/pedidos")]
public class PedidosController : ControllerBase
{
[HttpDelete("{id:int}")]
[Authorize(Policy = "pedidos:write")]
public IActionResult Delete(int id)
{
_pedidoService.Delete(id);
return NoContent();
}
}
|
app.MapDelete("/api/pedidos/{id:int}", (int id, IPedidoService pedidos) =>
{
pedidos.Delete(id);
return Results.NoContent();
}).RequireAuthorization("pedidos:write");
|
IAuthorizationService.AuthorizeAsync(user, pedido, "PedidoOwner"), que
avalia a política contra a entidade real.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("catalogo:read", policy =>
policy.RequireClaim("scope", "catalogo:read"));
options.AddPolicy("pedidos:write", policy =>
policy.RequireClaim("scope", "pedidos:write"));
});
|
public class CreateProdutoRequest
{
[Required]
[StringLength(200, MinimumLength = 1)]
public string Nome { get; set; } = string.Empty;
[Range(0.01, 1_000_000)]
public decimal Preco { get; set; }
}
[ApiController]
[Route("api/produtos")]
public class ProdutosController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateProdutoRequest request)
{
// Se chegou até aqui, o modelo já é válido
var produto = _produtoService.Create(request);
return CreatedAtAction(nomeof(Get), new { id = produtoo.Id }, produto);
}
}
|
app.MapPost("/api/produtos",
(CreateProdutoRequest request, IValidator<CreateProdutoRequest> validator) =>
{
var result = validator.Validate(request);
if (!result.IsValid)
{
return Results.ValidationProblem(result.ToDictionary());
}
var produto = produtoService.Create(request);
return Results.Created($"/api/produtos/{produto.Id}", produto);
});
|
// DTO de Requisição - apenas o que o cliente pode alterar public record UpdateProdutoRequest(string Nome, decimal Preco); [HttpPut("{id:int}")] public async Task<IActionResult> Update(int id, UpdateProdutoRequest request) { var produto = await _dbContext.Produtos.FindAsync(id); if (produto is null) { return NotFound(); } // Mapeia apenas os campos permitidos produto.Nome = request.Nome; produto.Preco = request.Preco; await _dbContext.SaveChangesAsync(); return NoContent(); } |
[HttpPost]
[Consumes("application/json")]
[RequestSizeLimit(1_000_000)] // 1 MB
public IActionResult Create(CreateProdutoRequest request)
{
var produto = _produtoService.Create(request);
return CreatedAtAction(nomeof(Get), new { id = produto.Id }, produto);
}
|
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 1_000_000;
});
|
[HttpGet]
public IActionResult GetProdutos(int page = 1, int pageSize = 20)
{
// Limita o tamanho da página para evitar que solicitem tudo de uma vez
pageSize = Math.Min(pageSize, 100);
var produtos = _produtoService.GetPage(page, pageSize);
return Ok(produtos);
}
|
';
DROP TABLE Produtos; --
passa a fazer parte da sua consulta. Consultas parametrizadas tratam a entrada
do usuário estritamente como dados, nunca como código SQL executável.
// 1. Seguro - O EF Core parametriza a variável 'search'
var produtos = await _dbContext.Produtos
.Where(p => p.Nome.Contains(search))
.ToListAsync();
|
// 2. Seguro - 'categoria' vira um parâmetro SQL, não texto concatenado var produtos = await _dbContext.Produtos .FromSqlInterpolated($"SELECT * FROM Produtos WHERE Categoria = {categoria}") .ToListAsync(); // 3. Perigoso - NUNCA faça isso var sql = "SELECT * FROM Produtos WHERE Categoria = '" + categoria + "'"; // 4. E NUNCA use FromSqlRaw com interpolação de strings var produtos = await _dbContext.Produtos .FromSqlRaw($"SELECT * FROM Produtos WHERE Categoria = {categoria}") .ToListAsync(); |
Program.cs:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.PermitLimit = 10;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter();
|
[HttpPost("login")]
[EnableRateLimiting("api")]
public IActionResult Login(LoginRequest request) { /* ... */ }
app.MapPost("/login", (LoginRequest request) => { /* ... */ })
.RequireRateLimiting("api");
|
(),
que permite que qualquer site na internet faça requisições para a sua API em
nome de um usuário autenticado.
builder.Services.AddCors(options =>
{
options.AddPolicy("Loja", policy =>
policy.WithOrigins("https://loja.exemplo.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type"));
});
var app = builder.Build();
app.UseCors("Loja");
|
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// Produção: resposta padronizada com ProblemDetails, sem stack trace
app.UseExceptionHandler();
}
|
Content-Security-Policy,
X-Content-Type-Options e
Referrer-Policy dizem ao navegador
como tratar suas respostas — quais scripts podem rodar, se deve adivinhar tipos
de conteúdo ou quanta informação de referência (referrer) deve vazar.
app.Use(async (context, next) =>
{
var headers = context.Response.Headers;
headers["X-Content-Type-Options"] = "nosniff";
headers["Referrer-Policy"] = "no-referrer";
headers["Content-Security-Policy"] = "default-src 'self'";
headers["X-Frame-Options"] = "DENY";
await next();
});
|
appsettings.json é um segredo vazado — ele
permanecerá no histórico do Git para sempre, visível para qualquer pessoa com
acesso ao repositório. Além disso, ao utilizar Agentes de IA, eles podem ler
diretamente os segredos das suas configurações, a menos que você negue o acesso.
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Loja" "Server=...;Password=..."
|
builder.Configuration.AddAzureKeyVault(
new Uri("https://loja-vault.vault.azure.net/"),
new DefaultAzureCredential());
var connectionString = builder.Configuration.GetConnectionString("Loja");
|
builder.Services.AddAntiforgery(options =>
{
options.HeaderNome = "X-CSRF-TOKEN";
});
var app = builder.Build();
app.UseAntiforgery();
|
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreatePedidoRequest request) { /* ... */ }
|
E estamos
conversados..![]()
"Para que possais andar dignamente diante do Senhor, agradando-lhe em tudo,
frutificando em toda a boa obra, e crescendo no conhecimento de Deus;"
Colossenses 1:10
Referências: