HIGH cache poisoningaxumjwt tokens

Cache Poisoning in Axum with Jwt Tokens

Cache Poisoning in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Cache poisoning in an Axum service that uses JWT tokens occurs when an attacker can influence cached responses in a way that causes the same malicious content to be served to other users. This typically arises when caching is applied at a layer that does not consider the authorization context carried in JWTs, such as a shared CDN or in-memory cache that key responses only by request path and query parameters.

In Axum, if you mount routes that return sensitive user data and rely on middleware that validates JWTs after routing or caching layers, a cached response for one authenticated user might be reused for another user who does not have permission to see that data. For example, a route like GET /api/users/me that is cached based solely on the path can return User A’s profile when User B requests it, because the cache key did not incorporate the JWT’s claims such as subject (sub) or roles. This violates authorization boundaries and can lead to information disclosure across users.

Another scenario involves query parameters that influence authorization but are not included in the cache key. If an endpoint accepts a query like ?include=roles and the response includes role information derived from the JWT, caching this response without binding it to the token’s claims can expose elevated privileges or sensitive metadata to unrelated clients. Because JWTs are often used to convey authorization decisions, failing to incorporate their contents into cache keys or validation logic creates a mismatch between the cached representation and the intended access control policies.

The risk is compounded when the JWT is used to skip authentication checks in cached routes, leading to situations where a public cache serves responses that were intended for authenticated contexts. Attackers may probe endpoints with different or missing tokens to see whether cached responses differ, potentially revealing whether authorization is enforced at the cache layer. This can expose user-specific data or business logic that should remain private. Proper mitigation requires tying cache entries to the token’s identity and claims, ensuring that distinct authorizations are not served from the same cached entry.

Jwt Tokens-Specific Remediation in Axum — concrete code fixes

To prevent cache poisoning in Axum when using JWT tokens, ensure that cache keys include relevant token claims and that authorization-sensitive responses are not shared across identities. Below are concrete code examples demonstrating how to bind JWT validation to route handling and cache awareness in Axum.

Example 1: JWT validation and cache-aware response handling

use axum::{
    async_trait, extract::Extension, routing::get, Router,
};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tower_http::cache::{CacheControlLayer, CacheLayer};
use tower_http::set_header::SetResponseHeaderLayer;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    roles: Vec,
}

async fn handler(
    Extension(state): Extension<AppState>,
    auth_header: Option<axum::http::HeaderValue>,
) -> Result<axum::Json<HashMap<String, String>>, (axum::http::StatusCode, String)> {
    let token = auth_header.ok_or((axum::http::StatusCode::UNAUTHORIZED, "Missing token".to_string()))?;
    let token_str = token.to_str().map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid token encoding".to_string()))?;

    let validation = Validation::new(Algorithm::HS256);
    let token_data = decode::<Claims>(token_str, &DecodingKey::from_secret(&state.jwt_secret), &validation)
        .map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid token".to_string()))?;

    let mut response = HashMap::new();
    response.insert("user_id".to_string(), token_data.claims.sub);
    response.insert("roles".to_string(), token_data.claims.roles.join(","));

    Ok(axum::Json(response))
}

#[tokio::main]
async fn main() {
    let app_state = AppState {
        jwt_secret: b"secret_secret_secret".to_vec(),
    };

    let app = Router::new()
        .route("/api/users/me", get(handler))
        .layer(Extension(app_state))
        .layer(CacheLayer::new())
        .layer(CacheControlLayer::new_no_store()); // Prevent caching of authenticated responses

    axum::Server::bind(&("0.0.0.0:3000".parse().unwrap()))
        .serve(app.into_make_service())
        .await
        .unwrap();
}

struct AppState {
    jwt_secret: Vec<u8>
}

Example 2: Incorporating JWT claims into a cache key (conceptual)

While Axum does not provide a built-in cache key builder, you can influence caching at the handler or middleware level by including the sub claim in response vary headers or by using a custom cache layer that uses a composite key of path and user ID extracted from the token.

use axum::{
    async_trait, extract::Extension, http::HeaderMap, routing::get, Router,
};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

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

async fn handler_user_profile(
    Extension(state): Extension<Arc<AppState>>,
    headers: HeaderMap,
) -> Result<String, (axum::http::StatusCode, String)> {
    let auth = headers.get("authorization")
        .and_then(|v| v.to_str().ok())
        .ok_or((axum::http::StatusCode::UNAUTHORIZED, "Missing authorization".into()))?;
    let token = auth.strip_prefix("Bearer ").ok_or((axum::http::StatusCode::UNAUTHORIZED, "Invalid auth format".into()))?;

    let validation = Validation::new(Algorithm::HS256);
    let data = decode::<Claims>(token, &DecodingKey::from_secret(&state.jwt_secret), &validation)
        .map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid token".into()))?;

    // Use claims.sub to differentiate cached entries per user
    let cache_key = format!("/api/profile/{}", data.claims.sub);
    // Here you would integrate with your caching backend using cache_key
    Ok(format!("Profile for user: {}", data.claims.sub))
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        jwt_secret: b"secret_secret_secret".to_vec(),
    });

    let app = Router::new()
        .route("/api/profile", get(handler_user_profile))
        .layer(Extension(state));

    axum::Server::bind(&("0.0.0.0:3000".parse().unwrap()))
        .serve(app.into_make_service())
        .await
        .unwrap();
}

struct AppState {
    jwt_secret: Vec<u8>
}

Best practices summary

  • Include JWT claims such as sub or roles in cache keys or vary headers for endpoints that return user-specific data.
  • Avoid caching authenticated responses at shared layers unless the cache explicitly accounts for the token’s identity.
  • Use Cache-Control: no-store for responses that contain sensitive user data to prevent accidental caching.
  • Validate JWT signatures and claims on every request and do not rely on cache to enforce authorization.

Frequently Asked Questions

Can cache poisoning occur if I use private in-memory caches in Axum?
Yes. Even in-memory caches can be vulnerable if they share entries across requests based only on path and query parameters. Always incorporate JWT claims such as subject or roles into your cache key to isolate user-specific responses.
Does middleBrick detect cache poisoning risks involving JWT tokens?
Yes. middleBrick scans endpoints that use JWT tokens and identifies cache poisoning risks where authorization context is not included in caching decisions. Findings include severity, impact, and remediation guidance mapped to frameworks such as OWASP API Top 10.