HIGH race conditionactixjwt tokens

Race Condition in Actix with Jwt Tokens

Race Condition in Actix with Jwt Tokens — how this specific combination creates or exposes the vulnerability

A race condition involving JWT tokens in an Actix-web service typically arises when token validity checks and state-changing operations are not performed atomically. Consider an endpoint that accepts a JWT access token and, based on claims such as scopes or roles, performs an action like updating a user’s email or elevating privileges. If the application first validates the JWT (for example, using jsonwebtoken or actix-web-jwt) and then issues a database or in-memory state update without a concurrency guard, an attacker can exploit timing differences between validation and state mutation.

For example, a user could present a valid JWT with scope read:emails, pass the authorization check, and then—before the request completes—revoke or rotate the token. If the application caches token validity or reuses a non-atomic check-then-set pattern (e.g., read from Redis, decide to proceed, then write a new scope), an attacker may execute the state change using a token that is concurrently being invalidated. This is a classic time-of-check-to-time-of-use (TOCTOU) issue in an asynchronous runtime like Actix, where many handlers execute concurrently on the same runtime threads.

In the context of JWT tokens, race conditions can also surface around token revocation lists or short-lived tokens with refresh flows. If Actix services use a denylist (e.g., Redis) to handle token revocation and a handler performs a non-atomic sequence of: (1) verify signature and expiry, (2) check denylist, (3) apply business logic, an attacker may revoke or replace the token after step 2 but before step 3. Because Actix handles requests concurrently, two requests with the same JWT can interleave in a way that allows an operation to proceed after revocation has begun but before it fully propagates.

Real-world attack patterns aligned with this include privilege escalation via token manipulation and unauthorized data access. While this is not directly described in the OAuth 2.0 RFC or JWT RFC as a named CVE, similar classes of concurrency issues map to OWASP API Security Top 10 controls such as broken object level authorization (BOLA) and improper authorization due to missing atomicity. The risk is elevated in high-concurrency Actix services that rely on fast in-memory caches or eventual-consistency stores for token state without strict ordering guarantees.

Jwt Tokens-Specific Remediation in Actix — concrete code fixes

Remediation focuses on making token validation and state changes atomic and deterministic. Prefer server-side session flags or distributed locks for critical operations, and avoid relying on token validity windows that can change between checks. When using JWT tokens in Actix, structure handlers so that authorization decisions are made from a single, consistent source of truth and are not separable from the state mutation.

Example 1: Atomic validation and update using a mutex keyed by user identifier to prevent concurrent state changes for the same JWT subject. This ensures that even with concurrent requests carrying the same JWT, only one proceeds to mutate state at a time.

use actix_web::{web, HttpResponse, Result};
use std::sync::Arc;
use tokio::sync::Mutex;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};

struct AppState {
    jwt_key: String,
    user_mutexes: Arc>>>,
}

async fn update_email_handler(
    token: String,
    body: web::Json,
    data: web::Data,
) -> Result {
    // 1) Validate JWT and extract subject in a single, trusted call
    let token_data = decode::(
        &token,
        &DecodingKey::from_secret(data.jwt_key.as_ref()),
        &Validation::new(Algorithm::HS256),
    ).map_err(|_| HttpResponse::Unauthorized().finish())?;
    let subject = token_data.claims["sub"].as_str().ok_or_else(|| HttpResponse::Unauthorized().finish())?;

    // 2) Acquire a per-user mutex to serialize mutations for this JWT subject
    let user_locks = data.user_mutexes.lock().await;
    let user_mutex = user_locks.get(subject).cloned().unwrap_or_else(|| {
        let m = Mutex::new(());
        // Note: in production, use a concurrent map like dashmap or reinsert under lock
        user_locks.insert(subject.to_string(), m.clone());
        m
    });
    drop(user_locks); // release the outer map lock before awaiting

    let _guard = user_mutex.lock().await;
    // 3) Perform state update atomically w.r.t. token authorization
    // e.g., update user email in DB; if token was revoked mid-request,
    // the revocation should be reflected in a shared store checked inside this critical section
    Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "ok" })))
}

Example 2: Centralized authorization check against a revocation-aware service before state change, ensuring the JWT and its associated state are validated together. This pattern avoids split checks and keeps revocation visibility fresh for each request.

async fn revoke_and_update_handler(
    token: String,
    new_email: web::Json,
    revocation_service: web::Data,
) -> HttpResponse {
    // Validate and extract claims
    let claims = match validate_jwt(&token) {
        Ok(c) => c,
        Err(_) => return HttpResponse::Unauthorized().finish(),
    };
    let subject = match claims.subject() {
        Some(s) => s,
        None => return HttpResponse::Unauthorized().finish(),
    };

    // Perform authorization and revocation check as a single logical unit
    if revocation_service.is_revoked(&subject, &claims.jti) {
        return HttpResponse::Forbidden().finish();
    }

    // Proceed only after consistent authorization; in practice, wrap DB ops in a transaction
    if let Err(_) = update_user_email(&subject, new_email.into_inner()).await {
        HttpResponse::InternalServerError().finish()
    } else {
        HttpResponse::Ok().json(serde_json::json!({ "email": new_email }))
    }
}

Additional guidance: keep JWT validation logic centralized, short-lived, and paired with revocation checks that are authoritative (e.g., a distributed cache with TTL aligned to token expiry). Avoid long-lived caches of "valid" tokens without invalidation hooks. These patterns reduce the window for race conditions in Actix services that rely on JWT tokens for access control.

Frequently Asked Questions

Can a race condition with JWT tokens in Actix lead to privilege escalation?
Yes. If token validation and authorization are split, an attacker may change scopes or roles between the check and the state update, leading to privilege escalation.
How does using a per-user mutex in Actix mitigate JWT token race conditions?
A per-user mutex serializes concurrent handlers for the same subject, ensuring that authorization and state mutation occur atomically and preventing interleaved execution that could exploit timing differences.