Log Injection in Axum with Jwt Tokens
Log Injection in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Log injection occurs when untrusted input is written directly into log entries, enabling attackers to forge log lines, inject new entries, or corrupt log metadata such as timestamps and severity levels. In Axum applications that rely on JWT tokens for authentication and authorization, log injection often arises when token payloads or related request attributes are embedded into logs without validation or sanitization.
Consider an Axum handler that authenticates a request by decoding a JWT and then logs the subject claim to track user actions:
use axum::{routing::get, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use std::net::SocketAddr;
async fn handler(
headers: axum::http::HeaderMap,
) -> Result<impl tower_http::response::IntoResponse, (axum::http::StatusCode, String)> {
let auth = headers.get("authorization").ok_or((axum::http::StatusCode::UNAUTHORIZED, "missing auth"))?;
let token = auth.to_str().map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "invalid auth string"))?;
let token_data = decode::<Claims>(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256))?;
let user_id = token_data.claims.sub;
// Potential log injection point:
tracing::info!("User accessed endpoint: user_id={}", user_id);
Ok(axum::Json("ok"))
}
If the sub claim in the JWT contains newline characters, structured logging frameworks may treat them as line breaks, allowing an attacker to inject additional log entries. For example, a token with sub: "12345\n[CRITICAL] forced=true" could produce two log lines, the second of which may be interpreted as a high-severity event by log-based monitoring tools. Similarly, special characters such as carriage returns or control characters can disrupt log parsing and aggregation, complicating incident response and audit trails.
Another scenario involves logging the entire token or parts of it for debugging purposes. Printing the full JWT (which includes header, payload, and signature) without sanitization can inadvertently expose sensitive data and provide a vector for injection if the token is later echoed by log processors that treat input as structured data.
LLM/AI Security considerations also intersect here: if logs include model-generated tokens or responses, output scanning for API keys and executable code becomes relevant to prevent attackers from exfiltrating secrets through log channels.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Remediation focuses on preventing untrusted data from corrupting log structure and ensuring that log entries remain atomic and parseable. The primary strategy is input validation and controlled formatting before logging.
1. Validate and sanitize JWT claims before logging
Reject or normalize claims that contain control characters. For the sub claim, enforce a pattern that excludes newlines and carriage returns:
use regex::Regex;
fn sanitize_sub(sub: &str) -> Result<String, (axum::http::StatusCode, String)> {
let re = Regex::new(r"^[\w-]+$").unwrap();
if re.is_match(sub) {
Ok(sub.to_string())
} else {
Err((axum::http::StatusCode::UNAUTHORIZED, "invalid subject claim"))
}
}
async fn handler(
headers: axum::http::HeaderMap,
) -> Result<impl tower_http::response::IntoResponse, (axum::http::StatusCode, String)> {
let auth = headers.get("authorization").ok_or((axum::http::StatusCode::UNAUTHORIZED, "missing auth"))?;
let token = auth.to_str().map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "invalid auth string"))?;
let token_data = decode::<Claims>(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256))?;
let user_id = sanitize_sub(&token_data.claims.sub)?;
tracing::info!("User accessed endpoint: user_id={}", user_id);
Ok(axum::Json("ok"))
}
This ensures that the logged identifier is a simple alphanumeric string, eliminating newline-based injection.
2. Use structured logging with explicit field types
When using tracing or similar crates, pass values as structured fields rather than interpolating them into message templates. This keeps the log event format stable and prevents injected newlines from creating additional entries:
tracing::info!(
target: "api_access",
"user_access",
user_id = %token_data.claims.sub,
method = %request.method(),
path = %request.uri().path()
);
Even if sub contains unexpected characters, structured logging backends can handle them more safely than plain-text concatenation.
3. Avoid logging raw tokens
Do not log the full JWT or its segments. If auditing is required, log only the claims you need (e.g., subject and issued-at) after validation:
tracing::debug!(
"Token validated",
user_id = %token_data.claims.sub,
exp = %token_data.claims.exp
);
Additionally, consider masking or hashing sensitive identifiers in non-production environments to reduce exposure risk.
These practices align with secure logging guidance and help ensure that authentication-related events remain reliable for monitoring and forensic analysis.