Mert Özen Further With Every Line
Backend .NET Core Web API — #22

CORS, Rate Limiting and Security Headers: Hardening the API Against External Threats

Mert Özen Aug 15, 2026 13 min 31 views
CORS, Rate Limiting and Security Headers: Hardening the API Against External Threats

We cover three topics that harden the API against misuse: controlling which sites can access it with CORS, limiting requests with rate limiting, and adding basic protection with security headers.

Identity and permission are now in place: we recognize the user, we control who can access what. But security isn't just about the "who can enter?" question. Your API is a door open to the outside world, and there will be those who want to misuse this door. Today, while closing the security block, we take on three important topics that harden the API against such threats: CORS, rate limiting, and security headers.

CORS: Which Sites Can Access Your API?

CORS means "Cross-Origin Resource Sharing." It sounds complex, but the problem it solves is simple: controlling whether a site running in a web browser can make a request to an API at a different address. Browsers, for security reasons, block a site from making a request to a different origin by default. CORS is the way to loosen this block deliberately.

Think of it like a building's visitor list. The security guard doesn't let everyone in; they only accept the names on the list. CORS works like this too: you tell your API "allow requests coming from these sites, reject the rest." So while your frontend can access your API, a site you don't recognize can't make the same request from the browser.

In .NET, you first register CORS as a service, then define a policy:

builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
    {
        policy.WithOrigins("https://mysite.com")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

This policy allows only requests coming from the address https://mysite.com. Then you add it to the pipeline:

app.UseCors("Frontend");

An important warning: sometimes you'll see AllowAnyOrigin in examples, which allows everywhere. While it's practical when learning, using this in a real project is a security hole; it means opening the door to everyone. Always explicitly specify the allowed addresses. Let's also clarify this: CORS is a browser rule, not an authentication mechanism. That is, CORS protects your API from browser-based abuse, but it's not a sufficient security layer on its own; it doesn't replace authentication.

Rate Limiting: Limiting Requests

If your API is an open door, someone can knock on that door repeatedly, hundreds of times per second. Maybe a malicious attack, maybe a faulty application stuck in a loop. In both cases, your server chokes and real users can't get service. Rate limiting counters this threat by limiting how many requests a user can make in a certain time.

Think of it like a turnstile at a gate. The turnstile allows a certain number of people to pass in a certain time; no one can force the door and start a rush. Rate limiting sets up such a turnstile in front of the API: you put a limit like "at most one hundred requests per minute," and requests exceeding this limit are temporarily rejected.

.NET supports rate limiting built-in. A simple fixed window limit is set up like this:

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("Fixed", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
    });
});

This setting allows at most one hundred requests in a one-minute window. When the limit is exceeded, the API automatically returns the 429 Too Many Requests status code; this code is the standard way of saying "you made too many requests, slow down a bit." Then you add this limiter to the pipeline:

app.UseRateLimiter();

There are different limiting strategies (like fixed window, sliding window, token bucket), and you choose according to your need. But the basic idea is the same in all of them: don't let any single user consume the entire system.

Security Headers: Small but Effective Protection

Every HTTP response carries headers alongside its body. Some of these headers instruct the browser to "apply these security measures." Adding the right security headers provides a cheap and effective defense against many common attacks. A few important ones are these:

X-Content-Type-Options prevents the browser from trying to guess a file's type; this closes some sneaky attacks. X-Frame-Options prevents the site from being secretly embedded inside another page (an attack called clickjacking). Strict-Transport-Security tells the browser "always establish a secure connection with this site."

A simple way to add these headers is to write a small middleware:

app.Use(async (context, next) =>
{
    context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    context.Response.Headers["X-Frame-Options"] = "DENY";
    await next();
});

This middleware adds these security headers to every response. Recall the middleware logic we learned in the thirteenth article: this code works like a door every request passes through, and on the way out it attaches the headers to the response. A small addition, but it seriously contributes to defense in depth.

Security Isn't One Thing, It's Layers

Today's three topics have a common lesson: security isn't provided by a single measure, it consists of layers. Authentication is one layer, authorization is another; CORS, rate limiting, and security headers are separate layers too. None of them is enough on its own, but when they come on top of each other, they form a strong defense. This is usually called "defense in depth": even if one layer is breached, other layers wait behind it.

So thinking "I added JWT, security is done" is dangerous. JWT only resolves identity. Misuse, overload, browser-based attacks still require separate measures. A good developer thinks of security not as a single box but as a whole of complementary measures.

A Small Experiment

First add CORS to your project and allow only a specific address. Then try making a request to your API through the browser from a different origin (for example, a simple page running on another port); see how the browser blocks this request. Next set up rate limiting, keep the limit deliberately low (for example, five requests per minute), and observe the 429 response with your own eyes by making consecutive requests to the same endpoint. Finally, add the security headers middleware and examine the headers added to the response from the browser's developer tools. These three experiments make abstract security concepts tangible.

With this article, we've completed the security and identity block. In the next article we move on to the quality and deployment block. The first topic will be documentation that explains how others will use your API: we'll talk about giving your API an automatic, interactive guide with Swagger/OpenAPI and Scalar. Your API is now secure; next is making it understandable and usable.