HIGH replay attackaxumjwt tokens

Replay Attack in Axum with Jwt Tokens

Replay Attack in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability

A replay attack occurs when an attacker intercepts a valid JWT issued to a legitimate user and re-sends it to the API to impersonate that user. In Axum, this risk arises because JWT validation typically verifies signature and standard claims (exp, iss, aud) but may not enforce mechanisms that prevent the same token from being used more than once. Without additional protections, an intercepted token remains usable until it expires, enabling an attacker to replay requests such as fund transfers or privilege escalation.

The vulnerability is specific to the combination of Axum middleware choices and how JWTs are validated. Axum does not automatically enforce one-time-use semantics; developers must explicitly add replay protection, for example by maintaining a short-lived denylist of token identifiers (jti) or embedding per-request unique values (cnonce) and validating them on each call. If the application only checks exp and signature, replay attacks against JWTs become feasible. Network-level protections such as TLS are necessary but insufficient; they prevent passive eavesdropping but do not stop an attacker who has obtained a valid token.

Common insecure patterns in Axum include using the default jsonwebtoken crate to decode and validate without recording jti or nonce values, and not enforcing strict token binding. Attackers can capture a token from logs, network traces, or compromised clients, then replay authenticated requests to endpoints that accept JWTs without additional checks. This is especially risky for idempotent operations where repeated requests do not immediately appear erroneous. The OWASP API Security Top 10 and related guidance highlight broken object level authorization and security misconfiguration as relevant concerns when replay controls are absent.

Jwt Tokens-Specific Remediation in Axum — concrete code fixes

Remediation focuses on ensuring JWTs cannot be reused. Implement short expiration times, include and validate a unique token identifier (jti), and maintain a short-lived denylist or store nonces for idempotency checks. For stateful protections, use a fast in-memory store with TTL aligned to token lifetime; for distributed scenarios, consider a shared cache with atomic operations. Combine these with HTTPS and strict validation of claims.

Example Axum middleware that validates JWTs and checks jti against a denylist:

use axum::{async_trait, extract::FromRequestParts, http::request::Parts, response::Response, Json};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, sync::Arc, time::{SystemTime, UNIX_EPOCH}};
use tower_http::set_header::SetResponseHeaderLayer;
use std::net::SocketAddr;
use axum::routing::get;
use axum::Router;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    jti: String,
    exp: usize,
    iat: usize,
}

#[derive(Clone)]
struct AppState {
    decoding_key: DecodingKey,
    validation: Validation,
    denylist: Arc>>,
}

#[async_trait]
impl FromRequestParts for Claims
where
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result {
        let app_state = state.downcast_ref::().ok_or_else(|| ())
    }
}

async fn handler() -> &'static str {
    "ok"
}

#[tokio::main]
async fn main() {
    let denylist = Arc::new(std::sync::Mutex::new(HashSet::::new()));
    let state = AppState {
        decoding_key: DecodingKey::from_secret("secret".as_ref()),
        validation: { 
            let mut v = Validation::new(Algorithm::HS256);
            v.validate_exp = true;
            v
        },
        denylist: denylist.clone(),
    };

    // In a real app, a background task would prune expired jti entries
    let app = Router::new()
        .route("/protected", get(handler))
        .layer(axum::middleware::from_fn_with_state(
            state.clone(),
            |state, request, next| async move {
                // extract Authorization: Bearer 
                let token = match request.headers().get("authorization") {
                    Some(h) => h.to_str().unwrap_or("").strip_prefix("Bearer ").unwrap_or(""),
                    None => return Err(axum::http::StatusCode::UNAUTHORIZED),
                };
                let token_data = decode::(token, &state.decoding_key, &state.validation)
                    .map_err(|_| axum::http::StatusCode::UNAUTHORIZED)?;
                let claims = token_data.claims;
                // Reject if jti is in denylist (replay)
                let mut denylist = state.denylist.lock().unwrap();
                if denylist.contains(&claims.jti) {
                    return Err(axum::http::StatusCode::UNAUTHORIZED);
                }
                // Insert jti with TTL aligned to token lifetime (example: 5 minutes)
                denylist.insert(claims.jti.clone());
                // In production, use a TTL store to auto-expire entries
                let response = next.run(request).await;
                Ok(response)
            },
        ))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Example Axum handler showing nonce/cnonce expectation for replay-safe operations:

use axum::{routing::post, Json, Router};
use serde::Deserialize;
use std::collections::HashSet;
use std::sync::Arc;

#[derive(Deserialize)]
struct TransactionRequest {
    amount: u64,
    nonce: String, // client-supplied unique value per request
}

#[derive(Clone)]
struct NonceStore(Arc>>);

async fn submit_transaction(
    NonceStore(store): NonceStore,
    Json(payload): Json,
) -> &'static str {
    let mut set = store.0.lock().unwrap();
    if set.contains(&payload.nonce) {
        return "rejected: duplicate nonce";
    }
    set.insert(payload.nonce);
    // process transaction
    "accepted"
}

fn app() -> Router {
    let store = NonceStore(Arc::new(std::sync::Mutex::new(HashSet::new())));
    Router::new()
        .route("/tx", post(submit_transaction))
        .with_state(store)
}

Operational guidance: rotate signing keys periodically, set short expirations (e.g., 5–15 minutes), and prune denylist entries after their TTL. For high-security contexts, combine jti checks with one-time nonces and bind tokens to client fingerprints where feasible. These measures reduce the window for replay and align with secure JWT usage practices.

Frequently Asked Questions

Can a replay attack happen over HTTPS if the JWT is intercepted?
Yes. HTTPS protects the token in transit, but if an attacker obtains a valid JWT (e.g., from logs or a compromised client), they can replay it over HTTPS. Prevent this by using short expirations, jti denylists, and nonces.
Does middleBrick detect replay attack risks for JWT-based APIs?
middleBrick scans unauthenticated attack surfaces and includes checks related to authentication and authorization. It reports findings such as missing replay protection and provides remediation guidance; it does not fix or block requests.