HIGH rate limiting bypassaspnethmac signatures

Rate Limiting Bypass in Aspnet with Hmac Signatures

Rate Limiting Bypass in Aspnet with Hmac Signatures — how this specific combination creates or exposes the vulnerability

In ASP.NET APIs that use HMAC signatures for request authentication, rate limiting can be inadvertently bypassed when the signature is computed over a subset of the request that does not include a nonce or a per-request unique value. Without a unique element per request, identical requests produce identical signatures, enabling an attacker to reuse a signed payload to circumvent rate limits that rely only on endpoint paths or IP-based counters.

The vulnerability occurs because the rate limiter and the HMAC verifier operate on different dimensions of the request. The rate limiter may track requests by URL + IP, while the HMAC validation ensures integrity and authenticity of a known set of headers and the body. If the signed set excludes a timestamp or nonce, an attacker can repeatedly send the exact same signed request within the rate limit window, and the API will accept each copy as valid. This is especially effective when the API tolerates replay windows or does not enforce one-time-use semantics for the signed components.

Consider an endpoint that accepts financial commands. The client computes the HMAC over HTTP method, path, selected headers, and the JSON body, then sends the signature in a custom header. If the body does not contain a client-generated nonce or a strictly increasing timestamp, an attacker can capture a valid request/response pair and replay the exact same HTTP message. The server validates the signature successfully and processes the command, while the rate limiter counts only one request if it is configured to inspect the signature context or if the limit is applied after authentication rather than before. This mismatch between authentication and rate-limiting scope allows abuse such as transaction duplication or brute-force attempts masked as legitimate traffic.

Insecure design patterns exacerbate the issue. For example, using a static or session-level key without per-request randomness, including only stable headers in the signature, or failing to bind the signature to a short time window can turn HMAC-based authentication into a vector for bypassing rate controls. Attack patterns include signature replay, where captured traffic is repeated within the allowed time window, and algorithmic abuse, where the client probes acceptable skew to synchronize clocks and avoid rejection.

To detect this class of issue, scanners evaluate whether the signed request scope includes a unique or monotonic element, whether the server enforces replay protection, and whether rate limiting is applied consistently across authenticated and unauthenticated paths. Findings highlight missing nonce usage, weak time synchronization, and inconsistent limit application as risk factors that can degrade the security grade assigned to the endpoint.

Hmac Signatures-Specific Remediation in Aspnet — concrete code fixes

Remediation centers on ensuring that each signed request carries a unique, verifiable value and that rate limiting considers the full authenticated context. In ASP.NET, this means including a nonce or a strictly increasing timestamp in the data covered by the HMAC, validating freshness on the server, and applying rate limits before or in conjunction with authentication checks.

Below is a complete, syntactically correct example of a server-side HMAC validation in ASP.NET Core that incorporates a nonce and timestamp to prevent replay and support accurate rate limiting. The client must generate a nonce and include it in the JSON body and in the signature base string.

// Server-side HMAC validation in ASP.NET Core middleware or action filter
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

public class HmacValidationFilter : IAsyncActionFilter
{
    private readonly string _apiSecret;
    private const int ClockSkewSeconds = 30;

    public HmacValidationFilter(IOptions<HmacOptions> options)
    {
        _apiSecret = options.Value.ApiSecret;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (!context.HttpContext.Request.Headers.TryGetValue("X-API-Signature", out var signatureHeader))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        if (!context.HttpContext.Request.Headers.TryGetValue("X-API-Nonce", out var nonceHeader) ||
            !context.HttpContext.Request.Headers.TryGetValue("X-API-Timestamp", out var timestampHeader))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        var nonce = nonceHeader.FirstOrDefault();
        var timestampString = timestampHeader.FirstOrDefault();
        if (string.IsNullOrWhiteSpace(nonce) || string.IsNullOrWhiteSpace(timestampString))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        if (!long.TryParse(timestampString, out var timestamp))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (Math.Abs(now - timestamp) > ClockSkewSeconds)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // Read the request body as a string for signing; ensure it can be read again if needed
        context.HttpContext.Request.EnableBuffering();
        using var reader = new StreamReader(context.HttpContext.Request.Body, Encoding.UTF8, leaveOpen: true);
        var body = await reader.ReadToEndAsync();
        context.HttpContext.Request.Body.Position = 0;

        var payload = $"{context.HttpContext.Request.Method}:{context.HttpContext.Request.Path}:{nonce}:{timestamp}:{body}";
        var computedSignature = ComputeHmacSha256(payload, _apiSecret);

        if (!computedSignature.Equals(signatureHeader.FirstOrDefault(), StringComparison.OrdinalIgnoreCase))
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // Optionally track nonce/timestamp to prevent reuse (e.g., in a distributed cache)
        await next();
    }

    private static string ComputeHmacSha256(string message, string secret)
    {
        using var key = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        using var hm = key;
        var hash = hm.ComputeHash(Encoding.UTF8.GetBytes(message));
        return Convert.ToBase64String(hash);
    }
}

public class HmacOptions
{
    public string ApiSecret { get; set; }
}

Client-side signing example that includes a nonce and timestamp ensures each request is unique. This makes replayed signatures detectable and allows the server to enforce per-request freshness checks, which in turn lets rate limiters use the authenticated identity plus nonce or timestamp to scope limits accurately.

// Client-side signing in C#
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public static class HmacClient
{
    public static string ComputeSignature(string method, string path, string nonce, long timestamp, object body, string secret)
    {
        var bodyJson = JsonSerializer.Serialize(body);
        var payload = $"{method}:{path}:{nonce}:{timestamp}:{bodyJson}";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return Convert.ToBase64String(hash);
    }
}

// Usage
var nonce = Guid.NewGuid().ToString();
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var body = new { accountId = "123", amount = 100 };
var signature = HmacClient.ComputeSignature("POST", "/api/transfer", nonce, timestamp, body, "my-secret");

// Send request with headers:
// X-API-Signature: {signature}
// X-API-Nonce: {nonce}
// X-API-Timestamp: {timestamp}
// Content-Type: application/json
// Body: {"accountId":"123","amount":100}

To bind rate limiting to the authenticated context, configure the rate limiter to consider the combination of user identity (or API key), nonce, or timestamp. In ASP.NET Core, the policy can inspect the validated claims or a cache of recently used nonces to reject duplicates within a sliding window. This ensures that even if an attacker bypasses simple IP-based counters, each unique signed request is still subject to enforceable limits.

Related CWEs: resourceConsumption

CWE IDNameSeverity
CWE-400Uncontrolled Resource Consumption HIGH
CWE-770Allocation of Resources Without Limits MEDIUM
CWE-799Improper Control of Interaction Frequency MEDIUM
CWE-835Infinite Loop HIGH
CWE-1050Excessive Platform Resource Consumption MEDIUM

Frequently Asked Questions

Why does including a nonce in the HMAC help prevent rate limiting bypass?
Including a nonce (or a strictly increasing timestamp) in the HMAC scope ensures each signed request is unique. This prevents attackers from reusing a captured signature to make identical repeated requests that would otherwise evade rate limits based on static identifiers.
Can rate limiting be applied after HMAC validation without risking bypass?
Applying rate limits only after HMAC validation can allow bypass when the limit does not account for per-request uniqueness. Rate limits should consider the authenticated context and include nonce or timestamp tracking to prevent replay-based abuse.