HIGH api rate abusespring boothmac signatures

Api Rate Abuse in Spring Boot with Hmac Signatures

Api Rate Abuse in Spring Boot with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Rate abuse occurs when an attacker sends a high volume of requests to consume resources, disrupt service, or bypass usage limits. When Hmac signatures are used for request authentication in Spring Boot, weaknesses in how the signature and rate-limiting are implemented can amplify risk.

Hmac signatures bind a request to a shared secret and canonical parameters (HTTP method, path, query string, headers, and body). If rate limiting is applied only after signature verification and does not include stable, attacker-controlled elements such as the key ID or a derived client identifier, an attacker can rotate through many valid keys or sign many requests per key to exhaust server-side rate counters. Additionally, if the signature does not include a nonce or timestamp, or if these are not enforced consistently, an attacker may replay requests within the allowed time window, bypassing rate controls that rely only on IP or endpoint counts.

In Spring Boot, common patterns include a filter or interceptor that computes the Hmac and validates it before the request reaches the controller. If the rate limiter (e.g., a token bucket or fixed window algorithm) is applied per endpoint or per IP rather than per authenticated principal, an attacker who can generate valid signatures can drive up request counts faster than the limiter can respond. Further, if the server is permissive toward query parameters that do not affect the canonical string, an attacker can vary non-security query params to change the request URL without invalidating the signature, creating many distinct request signatures that still pass verification and consume processing resources.

Another subtle issue is clock skew and timestamp tolerance. If the server accepts a wide range of timestamps and does not enforce strict replay protection, an attacker can reuse captured signed requests within the allowed window. Combine this with a rate-limiting strategy that does not track request timestamps per key, and the system may allow bursts that exceed intended limits. Attack patterns such as token exhaustion, resource exhaustion, or leveraging high-cost endpoints (e.g., those that trigger downstream calls) become feasible when Hmac-based authentication is not tightly coupled with robust, key-aware rate limiting.

Hmac Signatures-Specific Remediation in Spring Boot — concrete code fixes

To mitigate rate abuse while preserving Hmac-based authentication, design the system to rate limit per authenticated principal and include anti-replay controls. Tie rate limits to a stable client identifier extracted from the signature or from a key ID included in a header, and enforce tight timestamp windows with one-time use nonces where appropriate.

Below is a concrete Spring Boot example that shows a filter computing Hmac validation and a key-aware rate limiter using a token bucket stored in a concurrent map for simplicity. In production, replace the in-memory store with a distributed cache such as Redis to share state across instances.

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class HmacRateLimitFilter extends OncePerRequestFilter {

    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String RATE_LIMIT_HEADER = "x-client-key"; // stable key ID included in request
    private static final int MAX_REQUESTS_PER_MINUTE = 60;
    private final Map<String, TokenBucket> buckets = new ConcurrentHashMap<>();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String keyId = request.getHeader(RATE_LIMIT_HEADER);
        if (!StringUtils.hasText(keyId)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing key identifier");
            return;
        }
        if (!isRateAllowed(keyId)) {
            response.sendError(HttpServletResponse.SC_TOO_MANY_REQUESTS, "Rate limit exceeded for key");
            return;
        }
        if (!validateHmac(request)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid signature");
            return;
        }
        filterChain.doFilter(request, response);
    }

    private boolean isRateAllowed(String keyId) {
        return buckets.computeIfAbsent(keyId, k -> new TokenBucket(MAX_REQUESTS_PER_MINUTE, 1, TimeUnit.MINUTES))
                .tryConsume();
    }

    private boolean validateHmac(HttpServletRequest request) throws IOException {
        String signatureHeader = request.getHeader("x-signature");
        String timestamp = request.getHeader("x-timestamp");
        String nonce = request.getHeader("x-nonce");
        if (!StringUtils.hasText(signatureHeader) || !StringUtils.hasText(timestamp) || !StringUtils.hasText(nonce)) {
            return false;
        }
        long now = System.currentTimeMillis();
        long requestTime; // assume UTC milliseconds since epoch
        // Enforce tight tolerance (e.g., 2 minutes) to prevent replays
        try {
            requestTime = Long.parseLong(timestamp);
        } catch (NumberFormatException e) {
            return false;
        }
        if (Math.abs(now - requestTime) > TimeUnit.MINUTES.toMillis(2)) {
            return false;
        }
        // Ensure nonce uniqueness per keyId+timestamp (store/validate in cache)
        String cacheKey = keyId + "|" + timestamp + "|" + nonce;
        // pseudo: if alreadySeen(cacheKey)) return false; markSeen(cacheKey)

        String canonical = buildCanonical(request);
        String expected = computeHmac(canonical, getSecretForKey(keyId));
        return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
                signatureHeader.getBytes(StandardCharsets.UTF_8));
    }

    private String buildCanonical(HttpServletRequest request) {
        String method = request.getMethod();
        String path = request.getRequestURI();
        String query = request.getQueryString();
        String headersToSign = request.getHeader("x-timestamp") + "\n" + request.getHeader("x-nonce");
        String body = readBody(request); // implement carefully, avoid double-reading
        return method + "\n" + path + "\n" + (query != null ? query : "") + "\n" + headersToSign + "\n" + body;
    }

    private String computeHmac(String data, byte[] secret) {
        try {
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(new SecretKeySpec(secret, HMAC_ALGORITHM));
            return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("Hmac error", e);
        }
    }

    private byte[] getSecretForKey(String keyId) {
        // fetch secret securely; in practice use a vault or encrypted store
        return Base64.getDecoder().decode("ZmFrZVNlY3JldF9rZXlf"); // placeholder
    }

    private String readBody(HttpServletRequest request) throws IOException {
        return request.getReader().lines().reduce("", String::concat);
    }

    static class TokenBucket {
        private final int capacity;
        private double tokens;
        private long lastRefillTimestamp;
        private final long refillPeriodMillis;
        private final double tokensPerPeriod;

        TokenBucket(int capacity, double tokensPerPeriod, TimeUnit period) {
            this.capacity = capacity;
            this.tokens = capacity;
            this.tokensPerPeriod = tokensPerPeriod;
            this.refillPeriodMillis = period.toMillis(1);
            this.lastRefillTimestamp = System.currentTimeMillis();
        }

        synchronized boolean tryConsume() {
            refill();
            if (tokens >= 1) {
                tokens -= 1;
                return true;
            }
            return false;
        }

        private void refill() {
            long now = System.currentTimeMillis();
            long elapsed = now - lastRefillTimestamp;
            if (elapsed > refillPeriodMillis) {
                long cycles = elapsed / refillPeriodMillis;
                tokens = Math.min(capacity, tokens + cycles * tokensPerPeriod);
                lastRefillTimestamp += cycles * refillPeriodMillis;
            }
        }
    }
}

Key points specific to Hmac and rate abuse:

  • Include a stable client identifier (key ID) in the rate-limit key so limits apply per authenticated principal rather than per IP or endpoint.
  • Enforce timestamp and nonce checks to prevent replay attacks that could inflate request counts without consuming additional key capacity.
  • Validate the canonical string consistently on the server; avoid including non-authoritative parameters that an attacker can vary to create distinct signatures that bypass application-layer rate logic.

These measures ensure that Hmac-signed requests are both authenticated and subject to effective, key-aware rate controls, reducing the risk of API rate abuse.

Frequently Asked Questions

What should be included in the Hmac canonical string to prevent replay and rate abuse?
Include HTTP method, request path, query string (normalized), selected headers (e.g., timestamp and nonce), and the request body. Avoid non-authoritative parameters that do not affect the signature but can vary to evade rate limits.
How can rate limiting be made effective when using Hmac signatures in Spring Boot?
Apply rate limits per authenticated principal by using a stable key identifier (e.g., x-client-key) extracted from the request or key metadata. Combine with timestamp and nonce validation to block replays and use a shared cache (e.g., Redis) for distributed enforcement across instances.