Skip to content
Back to skills

Dotnet Rate Limiting

ASecurity

Reviews ASP.NET Core rate-limiting coverage. Flags login/auth endpoints with no rate limiting (brute-force/credential-stuffing exposure), the background-jobs admin controller lacking a rate limit on its trigger endpoint, no application-layer AddRateLimiter baseline for public APIs, and rate-limit rejections that omit a Retry-After header. Outputs findings with pilot-dotnet rate-limiting standard IDs.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
developmentgoapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill dotnet-rate-limiting --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dotnet Rate Limiting?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dotnet Rate Limiting
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-dotnet-rate-limiting/badge)](https://www.skillsdirectory.com/skills/agenticpawan-dotnet-rate-limiting)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dotnet-rate-limiting
description: Reviews ASP.NET Core rate-limiting coverage. Flags login/auth endpoints with no rate limiting (brute-force/credential-stuffing exposure), the background-jobs admin controller lacking a rate limit on its trigger endpoint, no application-layer AddRateLimiter baseline for public APIs, and rate-limit rejections that omit a Retry-After header. Outputs findings with pilot-dotnet rate-limiting standard IDs.
when_to_use: rate limiting, AddRateLimiter, brute force, credential stuffing, login throttling, Retry-After header, fixed window limiter, sliding window limiter, token bucket limiter, concurrency limiter, 429 too many requests
---

## Standard IDs

| ID | Severity | What it checks |
|----|----------|-----------------|
| RL-001 | P0 | Login/auth endpoint has no rate limiting |
| RL-002 | P1 | Background-jobs admin trigger endpoint has no rate limit |
| RL-003 | P2 | No global `AddRateLimiter` baseline for public APIs |
| RL-004 | P2 | Rate-limit rejection doesn't return a `Retry-After` header |

---

## Check A — No rate limiting on login/auth endpoints (RL-001)

### Detection

Grep the login/token-issuance endpoint for a rate-limiting policy attached
(`.RequireRateLimiting(...)`). An unthrottled login endpoint is a direct brute-force/
credential-stuffing target — this matters even more once permissions are resolved
per-request from a live store (`dotnet-authorization`), since a compromised account has
immediate effect with no stale-token delay to notice it in.

### BAD — login endpoint with no throttling

```csharp
app.MapPost("/api/auth/login", async (LoginDto dto, IAuthService auth) =>
{
    var result = await auth.LoginAsync(dto.Email, dto.Password);
    return result.Succeeded ? Results.Ok(result.Token) : Results.Unauthorized();
});
// Unlimited login attempts per second from a single IP or account.
```

### GOOD — a dedicated, tighter rate-limit policy on auth endpoints

```csharp
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("auth", opt =>
    {
        opt.PermitLimit = 5;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.QueueLimit = 0; // reject immediately, don't queue login attempts
    });
});

app.MapPost("/api/auth/login", async (LoginDto dto, IAuthService auth) => { ... })
   .RequireRateLimiting("auth");
```

---

## Check B — Background-jobs admin trigger endpoint has no rate limit (RL-002)

### Detection

Check the `BackgroundJobsController`/admin endpoint from `dotnet-background-jobs` BGJ-003
for a rate-limiting policy in addition to its `[Authorize]` guard. Authorization proves
*who* can call it; rate limiting bounds the damage if that caller's credentials are
compromised or a script bug fires the trigger in a loop.

### BAD — authorized but unbounded

```csharp
[Authorize(Policy = Permissions.Jobs.Manage)]
[HttpPost("{name}/trigger")]
public IActionResult Trigger(string name)
{
    BackgroundJob.Enqueue(name); // one compromised admin token = unlimited job triggers
    return Accepted();
}
```

### GOOD — authorized and rate-limited

```csharp
[Authorize(Policy = Permissions.Jobs.Manage)]
[HttpPost("{name}/trigger")]
[EnableRateLimiting("admin-jobs")]
public IActionResult Trigger(string name)
{
    BackgroundJob.Enqueue(name);
    return Accepted();
}

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("admin-jobs", opt =>
    {
        opt.PermitLimit = 20;
        opt.Window = TimeSpan.FromMinutes(1);
    });
});
```

---

## Check C — No global rate-limiting baseline (RL-003)

### Detection

Check whether the API relies solely on infrastructure-level throttling (API Management,
Front Door) with nothing configured at the application layer via `AddRateLimiter`. Relying
only on infrastructure means local dev/test environments and any deployment path that
bypasses that infrastructure layer have no protection at all — defense-in-depth means the
app itself should have a sane global baseline regardless of what sits in front of it.

### BAD — no app-level limiter, entirely dependent on infrastructure

```csharp
// No AddRateLimiter() anywhere in Program.cs — protection exists only in APIM/Front Door config,
// invisible to this repo and bypassed entirely in local/dev environments.
```

### GOOD — a sane global baseline, with tighter per-endpoint policies layered on top (Checks A/B)

```csharp
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
        RateLimitPartition.GetFixedWindowLimiter(
            ctx.User.Identity?.Name ?? ctx.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
            _ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1) }));
});

app.UseRateLimiter();
```

---

## Check D — Rejection omits Retry-After header (RL-004)

### Detection

Check the `OnRejected` callback (or the limiter's default behavior) for whether it sets a
`Retry-After` header on the `429` response. Without it, a well-behaved client/SDK has no
signal for how long to back off before retrying, and may retry immediately, compounding
the load the limiter was meant to shed.

### BAD — 429 with no guidance for the caller

```csharp
options.OnRejected = (context, ct) =>
{
    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    return ValueTask.CompletedTask; // caller has no idea when to retry
};
```

### GOOD — Retry-After header included

```csharp
options.OnRejected = (context, ct) =>
{
    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    context.HttpContext.Response.Headers.RetryAfter = "60";
    return ValueTask.CompletedTask;
};
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…