Crlf Injection in Axum with Basic Auth
Crlf Injection in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject carriage return (CR, \r) and line feed (\n) characters into a header or status-line value. In Axum, this typically arises when user-controlled data is reflected in HTTP response headers without sanitization. When Basic Authentication is used, the Authorization header is parsed by Axum middleware or handlers, and parts of it (such as the username or password) may be echoed into downstream headers or logs. If these values are not validated and an attacker supplies \r\n in the credentials, injected content can break header parsing and enable header manipulation or response splitting.
Consider an Axum handler that reads the Basic Auth token, decodes it, and then includes the username in a custom response header for debugging or correlation purposes:
use axum::{
async_trait,
extract::Request,
response::Response,
routing::get,
Router,
};
use std::convert::Infallible;
async fn auth_handler(
auth: Option<axum::extract::auth::Authorization<axum::http::header::authorization::Basic>>
) -> Response {
let username = auth.map(|a| a.into_username()).unwrap_or_else(|| "anonymous".to_string());
// Dangerous: username may contain \r\n if input is maliciously crafted
let mut response = Response::new(axum::body::Body::from("ok"));
response.headers_mut().insert(
"X-User",
username.parse().unwrap_or_else(|_| "unknown".parse().unwrap()),
);
response
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(auth_handler));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
If an attacker sends a request like Authorization: Basic dGVzdDpcIn0= (which decodes to test:" with crafted line breaks in the raw credential), and the server reflects the username in a header without validation, the injected \r\n can split the header stream. This may enable injection of arbitrary headers, potentially aiding cache poisoning, HTTP response splitting, or misleading logging. In a security scan, such patterns are flagged under HTTP Smuggling, Header Manipulation, and Input Validation categories, and the findings map to OWASP API Top 10 and CWE-113 (Improper Neutralization of CR and LF.)
Note that Axum does not automatically sanitize inputs from Basic Auth; developers must treat these values as untrusted. Even when using middleware for authentication, reflected credentials or parsed fields should be validated against a strict allowlist and encoded before being placed in headers, cookies, or any other protocol-sensitive context.
Basic Auth-Specific Remediation in Axum — concrete code fixes
Remediation focuses on never reflecting untrusted data into protocol-sensitive locations and strictly validating inputs from Basic Auth. If you must include a value in a header, ensure it is sanitized, and prefer using opaque tokens or session identifiers instead of raw credentials.
Below are concrete Axum examples that demonstrate safe handling of Basic Auth credentials.
1) Reject or sanitize carriage returns and line feeds
Validate the decoded username and password before use. Reject or sanitize characters such as \r and \n.
use axum::{
async_trait,
extract::Request,
response::Response,
routing::get,
Router,
};
use std::convert::Infallible;
fn sanitize_header_value(value: &str) -> Option<String> {
if value.contains('\r') || value.contains('\n') {
return None;
}
Some(value.to_string())
}
async fn safe_auth_handler(
auth: Option<axum::extract::auth::Authorization<axum::http::header::authorization::Basic>>
) -> Response {
let username = auth
.map(|a| a.into_username())
.and_then(|u| sanitize_header_value(&u).map(|s| s.into()))
.unwrap_or_else(|| "anonymous".to_string());
let mut response = Response::new(axum::body::Body::from("ok"));
if let Some(val) = sanitize_header_value(&username) {
response.headers_mut().insert("X-User", val.parse().unwrap_or_else(|_| "unknown".parse().unwrap()));
}
response
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(safe_auth_handler));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
2) Avoid reflecting credentials in headers; use a session or token instead
Do not place raw Basic Auth fields into headers. If you need a user identifier for logging or tracing, generate a separate, safe value.
use axum::{
async_trait,
extract::Request,
response::Response,
routing::get,
Router,
};
use std::convert::Infallible;
use uuid::Uuid;
async fn token_based_handler(
auth: Option<axum::extract::auth::Authorization<axum::http::header::authorization::Basic>>
) -> Response {
let _credentials_verified = auth.is_some(); // perform verification as needed
let trace_id = Uuid::new_v4().to_string();
let mut response = Response::new(axum::body::Body::from("ok"));
response.headers_mut().insert(
"X-Trace-Id",
trace_id.parse().unwrap_or_else(|_| "unknown".parse().unwrap()),
);
response
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(token_based_handler));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
These patterns reduce the risk of response splitting and header injection. For broader protection, combine input validation with a defense-in-depth approach that includes strict Content-Security-Policy and careful logging practices.