HIGH axumrate limit bypass

Rate Limit Bypass in Axum

How Rate Limit Bypass Manifests in Axum

In Axum applications, rate limiting is typically implemented using middleware built on the tower layer system, often with crates like tower-limit or axum-rate-limiter. A common vulnerability arises when the rate limiter's key (e.g., client identifier) is derived from a request field that an attacker can manipulate. The most frequent pattern is IP-based limiting using SocketAddr from the connection, which fails when the application sits behind a reverse proxy (like Nginx, Cloudflare, or a load balancer).

Axum's ConnectInfo<T> extractor provides the remote address of the immediate peer. If an application naively uses this value without considering proxy headers (e.g., X-Forwarded-For), an attacker can bypass limits by spoofing that header. Each request appears to come from a different IP to the application, while the proxy actually forwards them from the same source.

use axum::{
    extract::ConnectInfo,
    http::StatusCode,
    response::Response,
    routing::post,
    Router,
};
use std::net::SocketAddr;

async fn vulnerable_handler(
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> Result<Response, StatusCode> {
    // RATE LIMITER KEY IS `addr.ip()`
    // If behind a proxy, `addr` is the proxy's IP, not the client's.
    // All clients appear to come from the same proxy IP, so a global limit
    // is applied to all users. An attacker can set `X-Forwarded-For`
    // to unique values, but the limiter never sees them.
    // ...
    Ok(Response::new("OK"))
}

async fn vulnerable_route() -> Router {
    Router::new().route("/api", post(vulnerable_handler))
}

Another Axum-specific bypass occurs when rate limiting is applied only to specific routes or methods. An attacker can distribute requests across unmonitored endpoints (e.g., /api/v1/resource vs. /api/v2/resource) or use alternative HTTP methods (POST vs. GET) if the middleware is not applied globally. Axum's routing macros (route, nest) can lead to inconsistent middleware application if not carefully structured.

A third pattern involves stateless token buckets or fixed-window counters stored in process memory. In a multi-instance Axum deployment (common with tokio and hyper), each instance maintains its own counter. An attacker can target a single instance (e.g., by using a consistent X-Forwarded-For value that hashes to a specific backend) to exhaust its per-process limit, achieving a partial bypass even if a distributed store like Redis is used for global limits but incorrectly scoped.

Axum-Specific Detection

Detecting rate limit bypass in an Axum API requires testing how the application identifies clients and whether that identifier can be controlled. A scanner must probe multiple vectors:

  • Header Manipulation: Send requests with varying X-Forwarded-For, X-Real-IP, or Forwarded headers. If the rate limit resets or differs per unique header value, the application likely trusts these headers without validation.
  • Endpoint Distribution: Test rate limits across different routes and HTTP methods. If POST /api/login is limited but POST /api/auth/login is not, or if GET /resource has a separate limit from POST /resource, an attacker can split payloads.
  • Stateless Instance Targeting: For APIs behind a load balancer, attempt to pin requests to a single backend (if possible via consistent hashing) to test per-process limits versus global counters.

middleBrick's unauthenticated scan tests these patterns automatically. It sends sequential requests to the target endpoint, manipulating common proxy headers and varying request paths to observe changes in response status codes (e.g., 429 Too Many Requests) or timing. The scanner correlates responses to determine if the limit is tied to a mutable field. For Axum applications, the scan specifically checks for inconsistent 429 responses when X-Forwarded-For is rotated, and for missing limits on alternative routes discovered via OpenAPI spec analysis.

# Example of a middleBrick CLI scan for an Axum API
middlebrick scan https://api.example.com

# Output includes a rate limiting section:
# {
#   "rate_limiting": {
#     "bypassable": true,
#     "evidence": "429 returned only when X-Forwarded-For static; limit reset per new header value",
#     "severity": "high"
#   }
# }

The scanner also cross-references the OpenAPI/Swagger specification (if available) to identify all endpoints and methods, then probes each to ensure rate limiting middleware is uniformly applied. This is critical in Axum where developers might apply tower::limit::rate only to specific Router branches.

Axum-Specific Remediation

Fixing rate limit bypass in Axum requires two steps: (1) correctly identifying the client IP when behind a proxy, and (2) applying rate limiting consistently across all relevant routes.

1. Trusted Proxy IP Extraction: Use a crate like axum-extra::extract::Host or manually inspect X-Forwarded-For only if the immediate peer is a known, trusted proxy. Axum does not provide built-in proxy awareness; you must validate the ConnectInfo address against a list of trusted proxy IPs. The leftmost (original client) IP in X-Forwarded-For should be used only if the direct connection is from a trusted proxy.

use axum::{
    async_trait,
    extract::FromRequestParts,
    http::{request::Parts, StatusCode},
};
use std::net::IpAddr;

struct TrustedProxyRateLimiter {
    trusted_proxies: Vec<IpAddr>,
}

#[async_trait]
impl<S> FromRequestParts<S> for TrustedProxyRateLimiter
where
    S: Send + Sync,
{
    type Rejection = StatusCode;

    async fn from_request_parts(
        parts: &mut Parts,
        _state: &S,
    ) -> Result<Self, Self::Rejection> {
        let ConnectInfo(remote_addr) = parts
            .extract::<ConnectInfo<SocketAddr>>()
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

        // Check if the immediate peer is a trusted proxy
        if !Self::trusted_proxies().contains(&remote_addr.ip()) {
            // Not a proxy, use remote_addr directly
            return Ok(Self { /* use remote_addr.ip() as client_ip */ });
        }

        // Parse X-Forwarded-For header, take the leftmost non-private IP
        let client_ip = parts
            .headers
            .get("x-forwarded-for")
            .and_then(|h| h.to_str().ok())
            .and_then(|s| s.split(',').next())
            .and_then(|s| s.trim().parse().ok())
            .ok_or(StatusCode::BAD_REQUEST)?;

        Ok(Self { /* use client_ip */ })
    }
}

// Then use `TrustedProxyRateLimiter` as the key in your rate limiter.

2. Global Middleware Application: In Axum, ensure the rate limiting layer is applied to the root Router or to a nest that covers all API paths. Avoid applying it only to specific routes.

use axum::Router;
use tower::limit::rate::{RateLimit, RateLimiter};
use std::time::Duration;

// Create a rate limiter layer (e.g., 100 requests per minute per IP)
let rate_limiter = RateLimiter::new(
    // Your key extractor that uses TrustedProxyRateLimiter
    move |req: &Request| {
        // extract client IP safely
        // ...
    },
    Duration::from_secs(60),
    100,
);

let app = Router::new()
    .route("/api/login", post(login))
    .route("/api/data", get(get_data))
    // Apply rate limiting to ALL routes under `/api`
    .layer(tower::layer::layer_fn(|service| rate_limiter.layer(service)));

// Alternatively, use `nest` to apply to a subtree
let api_router = Router::new()
    .route("/login", post(login))
    .route("/data", get(get_data))
    .layer(rate_limiter);

let app = Router::new().nest("/api", api_router);

3. Distributed State: For multi-instance Axum deployments, use a shared store (Redis, Memcached) with a consistent key format (e.g., rate_limit:{client_ip}). Crates like axum-rate-limiter support Redis backends. Ensure the key includes both the client identifier and the rate limit scope (e.g., per endpoint if needed) to avoid collisions.

Finally, test your fix: after implementing trusted proxy IP handling and global middleware, rescan with middleBrick. The rate limiting finding should disappear, and the per-category score should improve.

Frequently Asked Questions

How can I manually test if my Axum API's rate limiting is bypassable?
Send a series of requests to the same endpoint while rotating the X-Forwarded-For header. If you receive 200 OK on each request without hitting a 429 Too Many Requests, the limit is likely bypassable because the application trusts the header. Use curl -H "X-Forwarded-For: 1.2.3.4" https://api.example.com/endpoint and increment the IP for each request.
Does applying rate limiting middleware to the root Router in Axum guarantee no bypass?
No. While applying middleware to the root Router ensures all routes are covered, bypass can still occur if the client identifier (IP) is incorrectly extracted. You must also validate proxy headers only when the immediate connection is from a trusted proxy. Without that validation, spoofing X-Forwarded-For will still create distinct keys.