Llm Data Leakage in Axum with Jwt Tokens
Llm Data Leakage in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
When an Axum API protected by JWT tokens exposes an LLM endpoint without proper authorization checks, sensitive data can leak in several concrete ways. Consider an Axum handler that decodes a JWT to obtain a user_id, then passes that identifier into a prompt sent to an unauthenticated LLM endpoint. If the LLM is reachable without any authentication, an attacker can probe the endpoint directly, sometimes triggering system prompt leakage patterns that reveal internal routing logic or data handling rules. Even when the JWT validates a legitimate user, the handler may concatenate user-specific context (such as tenant ID, role, or PII) into the LLM input. If that input is echoed back in LLM responses or logged in verbose error traces, confidential information can be exfiltrated through crafted prompts that ask the model to reveal its instructions or training details.
A realistic risk scenario involves an Axum service that decodes JWTs using jsonwebtoken and conditionally routes to an LLM inference path. Suppose the route does not enforce authentication for the LLM call and the prompt includes user metadata parsed from the token. An attacker can send a crafted request with a valid but low-privilege JWT and use active prompt injection techniques—such as asking the model to ignore prior instructions or to output its system message—to coax the LLM into revealing how it processes authorization data. Because middleBrick’s LLM/AI Security checks include system prompt leakage detection (27 regex patterns covering ChatML, Llama 2, Mistral, and Alpaca formats) and active prompt injection testing (five sequential probes including system prompt extraction and data exfiltration), this class of issue is detectable before it reaches production.
Another vector arises when token introspection or claims parsing is incomplete. An Axum handler might trust the decoded JWT payload without verifying scopes or roles, allowing a low-privilege caller to submit parameters that steer the LLM toward outputs containing sensitive training data or internal identifiers. If the LLM endpoint is unauthenticated, the attacker does not even need a valid token; they can send requests that mimic authorized traffic patterns. middleBrick’s unauthenticated LLM endpoint detection and output scanning for PII, API keys, and executable code help surface these weaknesses by correlating runtime behavior with OpenAPI/Swagger specs, including full $ref resolution, so that mismatches between declared authentication and actual exposure are clearly identified.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Secure integration of JWT validation with LLM pathways in Axum requires explicit authentication gating, strict claim verification, and isolation of sensitive data from prompts. Below are concrete, working examples that demonstrate how to implement this correctly.
// Cargo.toml dependencies
// axum = "0.7"
// jsonwebtoken = "0.8"
// serde = { version = "1.0", features = ["derive"] }
// tower-http = { version = "0.6", features = ["cors"] }
use axum::{routing::post, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
tenant_id: String,
scope: String,
exp: usize,
}
async fn llm_handler(
axum::Json(payload): axum::Json,
// JWT extracted from Authorization header
token: String,
) -> Result, (axum::http::StatusCode, String)> {
// 1) Validate and decode JWT with strict algorithm and issuer checks
let token_data = decode::(
&token,
&DecodingKey::from_secret(std::env::var("JWT_SECRET").unwrap_or_default().as_bytes()),
&Validation::new(Algorithm::HS256),
).map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid token".to_string()))?;
// 2) Enforce scope/role requirements before allowing LLM access
if token_data.claims.scope != "llm_access" {
return Err((axum::http::StatusCode::FORBIDDEN, "Insufficient scope".to_string()));
}
// 3) Build prompt without injecting tenant_id or other sensitive identifiers
let safe_prompt = format!("Answer the user query: {}", payload["query"]);
// Do NOT include tenant_id, sub, or other claims in the prompt
// 4) Call the LLM endpoint with explicit authentication (if required)
// Example using reqwest; ensure the LLM endpoint is protected and credentials are managed securely
let client = reqwest::Client::new();
let llm_response = client
.post("https://api.example.com/llm")
.bearer_auth(std::env::var("LLM_API_TOKEN").unwrap_or_default())
.json(&serde_json::json!({
"prompt": safe_prompt,
"max_tokens": 100
}))
.send()
.await
.map_err(|_| (axum::http::StatusCode::SERVICE_UNAVAILABLE, "LLM unreachable".to_string()))?;
let llm_body: serde_json::Value = llm_response.json().await.map_err(|_| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Failed to parse LLM response".to()))?;
Ok(axum::Json(llm_body))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/chat", post(llm_handler))
.layer(axum::middleware::from_fn(|_req, next| async move { next().await }));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Key practices reflected in the example:
- Validate JWTs with a strict algorithm and verify claims such as scope before routing to any LLM path.
- Never concatenate raw JWT claims or tokens into LLM prompts; keep sensitive metadata out of model input.
- Require authentication and credentials for the LLM endpoint itself, avoiding unauthenticated exposure.
- Leverage middleBrick’s CLI (middlebrick scan
) and GitHub Action to enforce security gates in CI/CD, ensuring that any regression in authentication or prompt handling fails the build.
Related CWEs: llmSecurity
| CWE ID | Name | Severity |
|---|---|---|
| CWE-754 | Improper Check for Unusual or Exceptional Conditions | MEDIUM |