Segurança em APIs ASP.NET Core: 16 práticas essenciais no .NET 10

A segurança de APIs não é uma funcionalidade adicionada ao final do projeto, mas um conjunto de decisões distribuídas por todo o pipeline de requisição. Afinal, arquitetura limpa e consultas otimizadas perdem o valor quando um invasor consegue ler o pedido de outro usuário ou forjar um token de acesso.

Este artigo apresenta as práticas essenciais para proteger APIs REST construídas com ASP.NET Core no .NET 10, versão LTS suportada até novembro de 2028.

Por que a segurança de APIs começa na configuração

A maior parte das falhas em produção não decorre de ataques sofisticados, mas da ausência de configurações básicas. Por isso, a segurança de APIs depende muito mais de disciplina do que de ferramentas exóticas.

Pacotes NuGet desatualizados, validação ausente, CORS permissivo demais e segredos versionados no repositório respondem pela maioria dos incidentes. A boa notícia é que o ASP.NET Core oferece nativamente quase todos os mecanismos necessários para fechar essas lacunas.

HTTPS e HSTS: protegendo o canal

Na segurança de APIs, toda requisição destinada à sua API deve trafegar sobre uma conexão criptografada. Sem HTTPS, tokens, credenciais e dados pessoais circulam em texto claro, e qualquer agente no caminho de rede consegue lê-los.

Além disso, o tráfego em HTTP puro viabiliza ataques de downgrade, nos quais o invasor força o cliente a usar uma conexão insegura. Para tratar esse cenário, o framework oferece dois mecanismos complementares.

O middleware UseHttpsRedirection redireciona requisições HTTP para HTTPS. Enquanto isso, o HSTS instrui o navegador a nunca mais abrir conexões sem criptografia com aquele domínio.

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();

O valor padrão de MaxAge no ASP.NET Core é de trinta dias, e muitas implantações elevam esse período para um ou dois anos. No entanto, o HSTS é um compromisso unidirecional: depois de receber o cabeçalho, o navegador recusa HTTP durante todo o prazo configurado.

Por isso, a recomendação oficial é começar com um MaxAge curto e aumentá-lo conforme a confiança na configuração cresce. Em desenvolvimento, o HSTS costuma ficar desligado, já que o localhost normalmente usa HTTP.

Autenticação com tokens JWT

Na segurança de APIs modernas, prefira autenticação stateless por tokens em vez de sessões guardadas no servidor. Sessões prendem o usuário a uma instância e dificultam o escalonamento horizontal, enquanto um token carrega a própria prova de identidade.

Para a maioria das APIs, isso significa tokens JWT emitidos por um provedor de identidade via OAuth 2.0 ou OpenID Connect. Se o formato ainda for novidade, o artigo o que é JWT explica a estrutura do token em detalhes.

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://your-idp.example.com";
        options.Audience = "shop-api";
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

A documentação da Microsoft insiste em dois pontos que muita gente ignora. Primeiro, tokens de acesso devem usar chaves assimétricas, para que clientes validem a assinatura com a chave pública exposta nos endpoints well known.

Segundo, a API nunca deve gerar tokens de acesso a partir de uma requisição de usuário e senha, porque esse fluxo fica vulnerável a personificação e phishing. Assim, a geração manual de tokens fica restrita a testes, como no uso da ferramenta dotnet user-jwts.

Validação rigorosa do token

Para a segurança de APIs, aceitar um token sem verificá-lo por completo equivale a não ter autenticação alguma. Antes de confiar em um JWT, a API precisa conferir quem o emitiu, para quem ele foi destinado, se ainda está válido e se a assinatura é legítima.

Portanto, configure essas verificações de forma explícita em TokenValidationParameters, sem depender apenas dos padrões do framework.

.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = "https://your-idp.example.com",

        ValidateAudience = true,
        ValidAudience = "shop-api",

        // Com Authority configurada, as chaves públicas vêm dos metadados do provedor
        ValidateIssuerSigningKey = true,

        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromSeconds(30)
    };
});

Cada propriedade fecha uma brecha específica. A validação da chave impede a forja de tokens, a de emissor e público garante que o token pertence à sua API e a de tempo de vida rejeita tokens expirados.

A propriedade ClockSkew merece atenção especial, porque o valor padrão é de cinco minutos. Ou seja, um token expirado ainda passa por esse intervalo extra; reduzi-lo para trinta segundos deixa a expiração mais precisa, desde que os servidores sincronizem o relógio via NTP.

Quando a validação falha, a API responde 401 para tokens inválidos ou expirados e 403 quando o usuário autenticado não tem permissão. Além disso, a especificação HTTP determina que respostas 401 incluam o cabeçalho WWW-Authenticate.

Autorização por políticas na segurança de APIs

Autenticação identifica o usuário, enquanto autorização determina o que ele pode fazer. O atributo [Authorize] sozinho verifica apenas que existe alguém autenticado, o que raramente reflete as regras reais de negócio.

Regras como "apenas gerentes podem excluir pedidos" pedem autorização baseada em políticas. Desse modo, a segurança de APIs passa a refletir o domínio, e não só a presença de um login.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("orders:write", policy =>
        policy.RequireRole("Manager")
              .RequireClaim("permission", "orders:write"));
});

A mesma política vale tanto para controllers quanto para endpoints de Minimal APIs.

app.MapDelete("/api/orders/{id:int}", (int id, IOrderService orders) =>
{
    orders.Delete(id);
    return Results.NoContent();
})
.RequireAuthorization("orders:write");

Quando a regra depende do recurso, como permitir edição apenas ao dono do pedido, use autorização baseada em recurso com IAuthorizationService.AuthorizeAsync. Para ver o fluxo completo em Minimal APIs, confira o artigo sobre autenticação e autorização com JWT e Bearer em Minimal APIs.

Privilégio mínimo com escopos

Na segurança de APIs, cada cliente deve receber exatamente o acesso necessário para cumprir sua função. Por exemplo, um aplicativo móvel que só lê o catálogo não deveria carregar um token capaz de excluir pedidos.

O provedor de identidade define o escopo na emissão do token, e as políticas da API verificam esse escopo em cada chamada.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("catalog:read", policy =>
        policy.RequireClaim("scope", "catalog:read"));

    options.AddPolicy("orders:write", policy =>
        policy.RequireClaim("scope", "orders:write"));
});

O mesmo raciocínio vale para chaves de API e contas de banco de dados. Assim, se uma credencial vazar, o estrago fica limitado ao que ela podia fazer.

Exigindo autenticação por padrão

Uma prática defensiva adicional é exigir autenticação em toda a aplicação, invertendo a lógica de exposição.

var requireAuthPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

builder.Services.AddAuthorizationBuilder().SetFallbackPolicy(requireAuthPolicy);

Com SetFallbackPolicy, endpoints criados sem [Authorize] continuam protegidos. Dessa forma, um simples esquecimento deixa de expor dados.

Validação de entrada no .NET 10

Uma regra básica de segurança de APIs é tratar todo payload como hostil até que a validação prove o contrário. Na prática, a API deve rejeitar entradas malformadas na borda, antes que alcancem a lógica de negócio ou o banco.

Em controllers, o atributo [ApiController] já converte falhas de validação em respostas 400 no formato problem details. Já para Minimal APIs, o .NET 10 trouxe o pacote Microsoft.Extensions.Validation, que valida query, cabeçalhos e corpo com uma única chamada.

builder.Services.AddValidation();

public record CreateProductRequest(
    [Required] string Name,
    [Range(0.01, 1_000_000)] decimal Price);

A implementação descobre os tipos usados nos handlers e adiciona um endpoint filter de validação. Contudo, o AddValidation usa um source generator que só enxerga tipos do assembly onde você faz a chamada.

Se os endpoints morarem em outro assembly, a validação simplesmente não executa e requisições inválidas chegam ao handler. A solução é criar um método de extensão nesse assembly que chame AddValidation e invocá-lo a partir da aplicação principal.

Por fim, uma implementação de IProblemDetailsService registrada no container permite customizar as respostas de erro. O artigo sobre validação em Minimal APIs no .NET 10 aprofunda esse recurso.

Proteção contra over-posting

Nunca faça o binding do JSON de entrada direto sobre a entidade do banco. Caso contrário, o cliente consegue definir campos que jamais deveria controlar, como IsAdmin, Balance ou o identificador de outro usuário.

Esse problema, chamado over-posting ou mass assignment, é uma via clássica de escalonamento de privilégios. A defesa consiste em receber um DTO que expõe apenas os campos permitidos e mapear manualmente para a entidade.

public record UpdateProductRequest(string Name, decimal Price);

[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, UpdateProductRequest request)
{
    var product = await _dbContext.Products.FindAsync(id);
    if (product is null)
    {
        return NotFound();
    }

    product.Name = request.Name;
    product.Price = request.Price;

    await _dbContext.SaveChangesAsync();
    return NoContent();
}

Com isso, campos como CreatedAt, OwnerId ou IsFeatured ficam inacessíveis, porque o contrato de entrada simplesmente não os contempla.

Limites de tamanho e paginação

Do ponto de vista de segurança de APIs, um endpoint deve recusar tudo aquilo que não está preparado para processar. Afinal, aceitar um corpo ilimitado abre espaço para um upload de vários gigabytes esgotar a memória e derrubar o serviço.

[HttpPost]
[Consumes("application/json")]
[RequestSizeLimit(1_000_000)]
public IActionResult Create(CreateProductRequest request)
{
    var product = _productService.Create(request);
    return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}

O limite global padrão do Kestrel para o corpo da requisição é de cerca de 30 MB. Segundo a documentação de segurança do Kestrel, APIs que não recebem uploads grandes devem reduzir bastante esse valor.

O mesmo cuidado vale para o volume de dados que o cliente extrai. Por isso, endpoints de coleção devem impor paginação com tamanho máximo de página.

Consultas parametrizadas contra SQL injection

Outra regra clássica de segurança de APIs: nunca monte SQL concatenando strings com dados do usuário. A concatenação é a origem clássica da injeção de SQL, em que uma entrada bem elaborada passa a rodar como código.

Felizmente, o Entity Framework Core parametriza consultas LINQ por padrão, o que torna esse caminho seguro.

var products = await _dbContext.Products
    .Where(p => p.Name.Contains(search))
    .ToListAsync();

var byCategory = await _dbContext.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Category = {category}")
    .ToListAsync();

O método FromSqlInterpolated converte os valores interpolados em parâmetros reais de comando. Em contrapartida, usar FromSqlRaw com strings interpoladas ou concatenadas anula essa proteção, então evite esse padrão.

Rate limiting e segurança de APIs

Limitar a frequência de chamadas por cliente protege a API contra força bruta e negação de serviço. Para isso, o ASP.NET Core inclui um middleware nativo baseado em System.Threading.RateLimiting.

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();

Um detalhe esquecido com frequência: o código de rejeição padrão do middleware é 503. Portanto, em APIs públicas, configure 429 de forma explícita, como recomenda a documentação.

Recursos mais avançados particionam o limitador por IP, identidade ou chave de API, de modo que um único cliente não consuma a cota de todos. Também é possível usar OnRejected para adicionar o cabeçalho Retry-After e registrar logs de abuso.

Antes de ir para produção, rode testes de carga, porque limites mal dimensionados negam acesso a tráfego legítimo. O artigo sobre rate limiting no ASP.NET Core 10 mostra os algoritmos disponíveis.

CORS restritivo e segurança de APIs

Na segurança de APIs consumidas por navegadores, o CORS controla quais origens web podem chamar sua API. Nesse contexto, o atalho perigoso é AllowAnyOrigin, que autoriza qualquer site a chamar a API em nome de um usuário autenticado.

builder.Services.AddCors(options =>
{
    options.AddPolicy("Shop", policy =>
        policy.WithOrigins("https://shop.example.com")
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Authorization", "Content-Type"));
});

var app = builder.Build();
app.UseCors("Shop");

Vale lembrar que combinar AllowAnyOrigin com AllowCredentials é inseguro, e os próprios navegadores rejeitam essa configuração.

Ordem do middleware na segurança de APIs

A ordem de registro dos middlewares define a ordem de execução, o que torna esse detalhe crítico para a segurança de APIs. A documentação do Kestrel sugere a sequência abaixo para servidores expostos diretamente à internet.

app.UseHostFiltering();
app.UseHttpsRedirection();
app.UseHsts();
app.UseRateLimiter();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

Quando a aplicação roda atrás de um proxy reverso, o UseForwardedHeaders precisa vir antes de tudo, pois restaura o IP, o esquema e o host originais. Se ele executar tarde demais, os componentes seguintes enxergam a requisição como HTTP simples e falham de forma sutil.

Por outro lado, nunca habilite esse middleware em servidores de borda sem proxy confiável, porque qualquer cliente poderia falsificar o próprio IP. Além disso, chame o rate limiting depois de UseRouting sempre que usar políticas específicas por endpoint.

Erros e cabeçalhos de segurança da API

Em termos de segurança, uma resposta de erro deve ajudar o consumidor legítimo, e não entregar ao invasor um mapa dos internos da API. Páginas de exceção detalhadas expõem stack traces, versões de framework, caminhos de arquivo e trechos de SQL.

builder.Services.AddProblemDetails();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler();
}

O diagnóstico detalhado fica nos logs, um ambiente controlado e auditável, e nunca no corpo da resposta. Em seguida, cabeçalhos de segurança complementam a proteção quando um navegador pode renderizar as respostas.

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();
});

Em produção, convém delegar esses cabeçalhos a uma biblioteca mantida ou ao proxy reverso, com uma política de CSP ajustada à aplicação.

Gestão de segredos

Na segurança de APIs, chaves, connection strings e tokens nunca devem ficar no controle de versão. Um segredo comitado em appsettings.json permanece no histórico do Git para sempre, ao alcance de quem acessa o repositório.

Em desenvolvimento, o Secret Manager do .NET resolve o problema sem mudar a forma como a aplicação lê a configuração.

dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Shop" "Server=...;Password=..."

Já em produção, a recomendação é usar variáveis de ambiente ou um cofre gerenciado, como o Azure Key Vault.

builder.Configuration.AddAzureKeyVault(
    new Uri("https://shop-vault.vault.azure.net/"),
    new DefaultAzureCredential());

Um alerta atual merece registro: agentes de inteligência artificial com acesso aos arquivos do projeto conseguem ler segredos de configuração. Portanto, negue esse acesso de forma explícita.

CSRF e o modelo de autenticação

Na segurança de APIs, a proteção contra Cross-Site Request Forgery importa quando a autenticação usa cookies. Isso porque o ataque explora o fato de o navegador anexar cookies automaticamente a requisições vindas de outros sites.

builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";
});

var app = builder.Build();
app.UseAntiforgery();

APIs baseadas apenas em tokens bearer são amplamente imunes a CSRF, já que o navegador não anexa o cabeçalho Authorization sozinho. Além disso, o middleware de antiforgery valida tokens apenas em POST, PUT e PATCH.

A Microsoft documenta um vetor sutil nesse ponto. Quando o HttpMethodOverrideMiddleware opera em modo de campo de formulário antes do antiforgery, um POST pode virar DELETE e escapar da validação.

Versionamento, auditoria e dependências na segurança de APIs

O versionamento permite aposentar um contrato inseguro sem quebrar todos os clientes ao mesmo tempo. Ao marcar uma versão como obsoleta, a API informa essa condição nos cabeçalhos e oferece uma janela clara de migração.

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(2, 0);
    options.ReportApiVersions = true;
});

A auditoria completa a segurança de APIs no dia a dia. Falhas de autenticação, negações de autorização e padrões suspeitos devem gerar logs estruturados com a conta envolvida, a origem e a ação tentada.

Como esses registros contêm dados sensíveis, eles também exigem controle de acesso e política de retenção. Por fim, lembre que a maioria das violações explora vulnerabilidades já conhecidas e corrigidas.

dotnet list package --vulnerable --include-transitive
dotnet list package --outdated

Incluir essa verificação no pipeline de integração contínua, fazendo o build quebrar diante de uma dependência vulnerável, é uma das medidas de maior retorno.

Conclusão

A segurança de APIs REST não resulta de uma única configuração, mas da aplicação consistente de camadas de defesa. HTTPS com HSTS, validação completa de tokens, políticas com privilégio mínimo, entrada tratada como hostil e rate limiting formam a base de qualquer API pública.

Sobre essa base, a disciplina operacional sustenta a proteção ao longo do tempo. Isso inclui não vazar detalhes em erros, manter segredos fora do repositório, versionar contratos, auditar eventos e atualizar dependências.

Não é necessário implementar tudo de uma vez. Em geral, começar por HTTPS, validação de token, autorização e validação de entrada, e então avançar pelo restante, costuma ser a estratégia mais eficaz.

FAQ

Qual versão do .NET usar em uma API nova?

O .NET 10 é a versão LTS mais recente, com suporte até novembro de 2028. Versões LTS recebem no mínimo três anos de suporte e servem bem a aplicações de longa duração.

Preciso de proteção CSRF em uma API que usa JWT?

Na maioria dos casos, não, porque o navegador não anexa o cabeçalho Authorization automaticamente. A proteção se torna necessária quando há autenticação por cookies.

Por que reduzir o ClockSkew da validação de token?

O padrão de cinco minutos aceita tokens expirados por esse tempo extra. Reduzir para trinta segundos deixa a expiração mais precisa, desde que os relógios estejam sincronizados.

A validação nativa de Minimal APIs substitui o FluentValidation?

Depende das regras: a validação do .NET 10 cobre bem data annotations e IValidatableObject. Regras que dependem de consultas externas continuam mais simples com bibliotecas especializadas.

Qual status retornar quando o limite de requisições estoura?

O padrão do middleware é 503, mas APIs públicas devem retornar 429 Too Many Requests. Configure isso em RejectionStatusCode.

Cabeçalhos de segurança fazem sentido em uma API que só retorna JSON?

Fazem, ainda que com impacto menor. Eles custam praticamente nada e funcionam como defesa em profundidade quando um navegador renderiza a resposta.