Boas Práticas de Segurança em APIs ASP .NET Core
  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.

Não existe uma única funcionalidade que torna uma API completamente segura por si só. A segurança é um conjunto de práticas aplicadas continuamente ao longo de todo o ecossistema da aplicação.

 

Tópicos abordados neste artigo

Abaixo está um resumo do que vamos explorar em detalhes:

- Forçar HTTPS em todos os lugares
- Autenticar com tokens, não com sessões
- Validar a assinatura, o emissor (issuer), o público (audience) e o tempo de vida do JWT
- Autorizar com políticas (policies), não apenas com [Authorize]
- Aplicar o princípio do menor privilégio
- Validar e sanitizar todas as entradas
- Proteger contra over-posting / atribução em massa (mass assignment)
- Validar tipos de conteúdo e limitar o tamanho da requisição
- Usar consultas parametrizadas e o EF Core
- Implementar limitação de taxa (rate limiting) e throttling
- Configurar o CORS de forma restritiva
- Retornar o mínimo de detalhes de erro
- Definir cabeçalhos de segurança (security headers)
- Armazenar segredos com segurança
- Impor proteção contra CSRF onde for relevante
- Versionar sua API e descontinuar endpoints inseguros
- Registrar (log) e auditar eventos de segurança
- Manter as dependências atualizadas com patches

Vamos ao que interessa.

1. Forçar HTTPS em Todos os Lugares

Toda requisição para a sua API deve trafegar por uma conexão criptografada.

Sem HTTPS, tokens, senhas e dados pessoais transitam em texto puro, onde qualquer pessoa no caminho da rede pode lê-los. O HTTP simples também abre portas para ataques de downgrade, nos quais um atacante força um cliente a usar uma conexão insegura.

O ASP .NET Core oferece duas ferramentas. UseHttpsRedirection redireciona requisições HTTP para HTTPS, e HSTS (HTTP Strict Transport Security) instrui os navegadores a se conectarem apenas via HTTPS:

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

UseHttpsRedirection eleva qualquer requisição HTTP para HTTPS. UseHsts adiciona o cabeçalho Strict-Transport-Security, garantindo que navegadores compatíveis se recusem a conversar com sua API via HTTP comum — bloqueando ataques de downgrade.
O HSTS é ignorado durante o desenvolvimento porque você frequentemente usa http://localhost.

2. Autenticar com Tokens, Não com Sessões

Prefira autenticação por token stateless (sem estado) em vez de sessões no lado do servidor.

Uma sessão armazena o estado de autenticação na memória do servidor ou em um banco compartilhado, o que vincula cada usuário a um servidor específico e dificulta o dimensionomento horizontal (horizontal scaling). Um token carrega sua própria prova de identidade, permitindo que qualquer instância da sua API o valide sem precisar consultar uma base.

Para a maioria das APIs, isso significa usar JWT bearer tokens, frequentemente emitidos por um provedor de identidade via OAuth 2.0 ou OpenID Connect:

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

O cliente envia o token no cabeçalho
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.
Isso mantêm sua API stateless e fácil de dimensionar horizontalmente.

3. Validar a Assinatura, o Emissor, o Público e o Tempo de Vida do JWT

Antes de aceitar um token, você deve confiar nele.

Um token só é seguro quando você verifica quem o emitiu, para quem ele se destina, se ainda não expirou e se sua assinatura é válida. Configure essas checagens explicitamente através de TokenValidationParameters em vez de confiar nos padrões do framework:

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

Cada flag fecha uma brecha de segurança:

- ValidateIssuerSigningKey confirma a assinatura — nunca desligue isso, caso contrário, qualquer um poderá forjar um token.
- ValidateIssuer e ValidateAudience garantem que o token veio do seu provedor de identidade e foi destinado à sua API, não a outro serviço.
- ValidateLifetime rejeita tokens expirados.

Preste atenção especial ao ClockSkew. Ele existe para tolerar pequenas diferenças de relógio entre os servidores, mas seu padrão é de generosos 5 minutos. Isso significa que um token expirado ainda pode ser aceito por até 5 minutos adicionais. Reduzi-lo para cerca de 30 segundos (ou até zero) reforça o controle do tempo de expiração, desde que os relógios dos seus servidores estejam sincronizados (via NTP).

4. Autorizar com Políticas, Não Apenas com [Authorize]

A autenticação diz quem o usuário é. A autorização decide o que ele tem permissão para fazer.

Um simples atributo [Authorize] apenas verifica se o usuário está logado. Regras de negócio reais — como "apenas gerentes podem excluir pedidos" ou "usuários só podem ver seus próprios dados" — exigem autorização baseada em políticas (policies) e em recursos.

Defina uma política uma única vez:

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

Aplique-a em uma ação de controller:

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

Ou em um endpoint de Minimal API:

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

Quando a regra depender do recurso específico — como "apenas o dono pode editar este pedido" — use autorização baseada em recursos com IAuthorizationService.AuthorizeAsync(user, pedido, "PedidoOwner"), que avalia a política contra a entidade real.
As políticas mantêm a lógica de autorização em um único lugar, fora do corpo dos seus endpoints.

5. Aplicar o Princípio do Menor Privilégio

Dê a cada cliente o acesso mínimo de que ele precisa, e nada mais.

Um token, um conjunto de claims ou uma chave de API devem conceder exatamente as permissões necessárias para sua função. Um aplicativo móvel que apenas lê o catálogo de produtos não deve possuir um token capaz de deletar pedidos.

Defina esse escopo no nível do token. Quando seu provedor de identidade emite um token, ele deve incluir apenas os escopos concedidos ao cliente, e suas políticas devem checar por eles:

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

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

A mesma idéia se aplica a chaves de API e contas de banco de dados — limite seus escopos. Se uma credencial vazar, o princípio do menor privilégio limita o raio de impacto apenas ao que aquela credencial específica podia fazer.

6. Validar e Sanitizar Todas as Entradas

Trate qualquer carga útil (payload) recebida como perigosa até que se prove o contrário.

Entradas malformadas ou maliciosas devem ser rejeitadas na borda (edge), antes de atingirem a lógica de negócios ou o banco de dados. O ASP .NET Core facilita isso: com o atributo [ApiController], qualquer requisição que falhe na validação do modelo retorna automaticamente um status 400 com os detalhes do problema.

Adicione data annotations ou, para regras mais complexas, use o FluentValidation:

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

O equivalente em Minimal API executa a validação via filtro ou explicitamente:

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

Validar dados precocemente transforma toda uma classe de ataques — como strings gigantescas, números fora do intervalo ou campos ausentes — em uma resposta 400 limpa, em vez de gerar uma exceção profunda no seu código.

7. Proteger contra Over-Posting / Atribução em Massa (Mass Assignment)

Nunca vincule (bind) o JSON recebido diretamente à sua entidade do banco de dados.

Se os clientes enviarem dados diretamente para sua entidade do EF Core, eles poderão alterar campos que nunca deveriam controlar — como IsAdmin, Balance, Status ou o Id de outro usuário. Isso é chamado de over-posting ou atribução em massa (mass assignment).

Vincule a requisição a um DTO (Data Transfer Object) dedicado que exponha apenas os campos que o cliente tem permissão para definir. Em seguida, mapeie-o para a entidade manualmente:

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

A entidade Produto pode possuir campos como CriadoEm, ProprietarioId ou EstaAtivo, mas como o cliente só pode enviar Nome e Preco, esses outros campos ficam inacessíveis pelo lado externo.
Modelos de requisição separados exigem um pouco mais de código, mas fecham toda uma categoria de falhas de elevação de privilégio.

8. Validar Tipos de Conteúdo e Limitar o Tamanho da Requisição

Rejeite aquilo que você não está preparado para processar.

Um endpoint que aceita JSON deve rejeitar outros tipos de mídia, e nenhum endpoint deve aceitar um corpo de requisição sem limite — um upload de vários gigabytes é uma forma rápida de esgotar a memória do servidor e derrubar sua API.

Restrinja o tipo de conteúdo com [Consumes] e limite o tamanho do corpo com [RequestSizeLimit]:

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

Você também pode definir um limite global através do Kestrel:

builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 1_000_000;
});

A mesma disciplina se aplica à quantidade de dados que um cliente pode buscar. Sempre imponha paginação com um tamanho máximo de página nos endpoints de coleção, para evitar que um cliente solicite um conjunto de resultados ilimitado:

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

Limitar o tamanho da requisição, o tipo de conteúdo e o tamanho da página protege contra o mesmo risco: uma única requisição tentando consumir mais recursos do que o servidor pode oferecer.

9. Usar Consultas Parametrizadas e EF Core

Nunca monte instruções SQL concatenando strings com dados vindos do usuário.

A concatenação de strings é a porta de entrada para injeção de SQL (SQL Injection) — uma entrada maliciosa como ';
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.

O EF Core parametriza tudo por padrão, tornando consultas LINQ seguras:

// 1. Seguro - O EF Core parametriza a variável 'search'
var produtos = await _dbContext.Produtos
    .Where(p => p.Nome.Contains(search))
    .ToListAsync();

Caso precise usar SQL puro, utilize FromSql ou FromSqlInterpolated, que convertem os valores interpolados em parâmetro em vez de texto literal:

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

Os dois primeiros exemplos são seguros; o 3 e o 4 abrem brechas diretas para atacantes.

10. Implementar Limitação de Taxa (Rate Limiting) e Throttling

Limite a frequência com que um único cliente pode chamar sua API.

Sem limites, um único cliente — ou atacante — pode bombardear seu endpoint de login com tentativas de força bruta (brute-force), ou inundar sua API com requisições até que ela caia. O ASP .NET Core possui um rate limiter nativo configurável no 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();

Em seguida, aplique a política a um controller ou endpoint de Minimal API:

[HttpPost("login")]
[EnableRateLimiting("api")]
public IActionResult Login(LoginRequest request) { /* ... */ }

app.MapPost("/login", (LoginRequest request) => { /* ... */ })
   .RequireRateLimiting("api");

Quando o cliente excede o limite, recebe uma resposta 429 Too Many Requests sem chegar a executar o seu código. Isso inibe ataques de força bruta e negação de serviço (DoS).

11. Configurar o CORS de Forma Restritiva

Permita apenas que origens confiáveis façam chamadas para sua API a partir do navegador.

O CORS (Cross-Origin Resource Sharing) controla quais origens web podem realizar requisições para a sua API. O atalho perigoso é o
AllowAnyOrigin(), que permite que qualquer site na internet faça requisições para a sua API em nome de um usuário autenticado.

Defina uma política nomeada listando exatamente as origens, métodos e cabeçalhos permitidos:

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

Isso significa: apenas https://loja.exemplo.com pode realizar chamadas, apenas com os métodos e cabeçalhos especificados. Qualquer outra tentativa será recusada.
Reserve o AllowAnyOrigin exclusivamente para APIs públicas e não autenticadas — e nunca o combine com o envio de credenciais.

12. Retornar o Mínimo de Detalhes de Erro

A resposta de erro deve ajudar quem está chamando a API, não um atacante.

Uma página de exceção bruta expõe stack traces, versões de frameworks, caminhos de arquivos e consultas SQL — um mapa completo da sua estrutura interna. Em vez disso, retorne erros padronizados e limpos usando o formato Problem Details (RFC 9457):

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

Em produção, uma exceção não tratada retornará um corpo estruturado de Problem Details com um código de status e uma mensagem genérica — mantendo a arquitetura interna oculta.
Mantenha diagnósticos detalhados nos seus logs (onde apenas você tem acesso), nunca no corpo da resposta HTTP.

13. Definir Cabeçalhos de Segurança (Security Headers)

Alguns cabeçalhos de resposta protegem sua API contra ataques comuns no lado do navegador.

Cabeçalhos como 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.

Você pode adicioná-los usando um middleware simples executado a cada resposta:

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

- X-Content-Type-Options: nosniff: impede que o navegador adivinhe (e execute indevidamente) tipos de conteúdo.
- Content-Security-Policy: restringe de onde scripts, estilos e outros recursos podem ser carregados.
- Referrer-Policy: controla quanto da sua URL é enviada para outros sites.
- X-Frame-Options: DENY: evita que suas respostas sejam embutidas em frames (prevenindo ataques de clickjacking).

Nota: Em produção, considere utilizar uma biblioteca mantida pela comunidade ou um proxy reverso para gerenciar esses cabeçalhos e aplicar uma política de Content-Security-Policy mais estrita.

14. Armazenar Segredos com Segurança

Chaves, strings de conexão e tokens nunca devem ser mantidos no controle de versão.

Um segredo commitado no
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.

Mantenha os segredos completamente fora da base de código.

Em ambiente de desenvolvimento, utilize o .NET Secret Manager:

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

Em produção, utilize variáveis de ambiente ou um cofre de segredos gerenciado, como o Azure Key Vault:

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

var connectionString = builder.Configuration.GetConnectionString("Loja");

O código consome as configurações da mesma maneira, independentemente de onde o valor venha. A aplicação não se importa com a origem — desde que o dado não esteja salvo em um arquivo versionado.

15. Impor Proteção contra CSRF Onde For Relevante

A proteção contra Cross-Site Request Forgery (CSRF) é fundamental quando a autenticação é realizada via cookies.

O CSRF induz o navegador de um usuário logado a enviar uma requisição maliciosa não intencional, aproveitando-se do cookie que o navegador anexa automaticamente. Se a sua API autentica via cookies, você precisará de tokens anti-forgery:

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

var app = builder.Build();

app.UseAntiforgery();

Exija um token válido em endpoints que alterem o estado do sistema:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreatePedidoRequest request) { /* ... */ }

Um detalhe importante: APIs baseadas puramente em tokens são amplamente imunes ao CSRF. Se o cliente envia um JWT no cabeçalho Authorization (e não em um cookie), o navegador não o anexa automaticamente, o que significa que uma requisição forjada não carregará as credenciais. A proteção anti-forgery é necessária principalmente em endpoints autenticados por cookies.

Conclusão

A segurança não é um recurso isolado adicionado ao final do desenvolvimento. É um conjunto de hábitos praticados em toda a API.

Nenhuma prática isolada torna uma API 100% segura, e você não precisa aplicar todas de uma vez. Começe por aquelas que cobrem suas maiores lacunas — geralmente HTTPS, validação de token, autorização e validação de entrada — e vá avançando na lista.

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:


José Carlos Macoratti