Crlf Injection in Axum
How Crlf Injection Manifests in Axum
CRLF injection in Axum occurs when unvalidated user input containing carriage return ( ) or line feed ( ) characters is incorporated into HTTP headers, response bodies, or log messages. This attack vector exploits the fact that HTTP protocol parsing is sensitive to line breaks, allowing attackers to manipulate the structure of HTTP messages.
In Axum applications, CRLF injection commonly appears in these contexts:
- Header manipulation: User input reflected in custom headers or cookie values
- Response splitting: Crafting responses that inject additional headers or HTML content
- Log injection: Malicious input written to log files, potentially creating false entries
- Template injection: User data interpolated into HTML templates without sanitization
Consider this vulnerable Axum route handler:
use axum::http::HeaderValue;
use axum::response::IntoResponse;
async fn vulnerable_handler(
Path(user_input): Path<String>,
) -> impl IntoResponse {
let header_value = HeaderValue::from_str(&user_input).unwrap();
let mut response = axum::response::Response::builder()
.header("X-User-Input", header_value)
.body(user_input)
.unwrap();
response
}
If an attacker sends /api/vulnerable/Hello%0D%0ASet-Cookie:%20malicious=true, the response will include the injected header, potentially leading to session fixation or XSS attacks.
Another common pattern involves constructing Location headers from user input:
async fn redirect_handler(
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
let url = format!("https://example.com/redirect?{}&user={}",
params.get("tracking").unwrap(),
params.get("user").unwrap());
// Vulnerable: user input not validated for CRLF
axum::response::Redirect::found(url)
}
An attacker could inject %0D%0AContent-Length:%200%0D%0A%0D%0AHTTP/1.1%20200%20OK%0D%0AContent-Type:%20text/html%0D%0A%0D%0A%3Ch1%3EAttack%20Successful%3C/h1%3E to perform response splitting.
Axum-Specific Detection
Detecting CRLF injection in Axum applications requires both static analysis and runtime scanning. middleBrick's black-box scanning approach is particularly effective for this vulnerability.
middleBrick scans Axum APIs by:
- Sending payloads containing CRLF sequences to all endpoints
- Analyzing responses for unexpected headers, content types, or HTTP status codes
- Checking for HTTP response splitting indicators (multiple status lines, header injection)
- Verifying that user input is properly encoded before being included in headers
The scanner tests 27 different CRLF patterns including:
%0D%0A, %0A, %0D, \r\n, \n, \r,
,
,
For Axum applications specifically, middleBrick examines:
- Header construction patterns using
HeaderValue::from_str - Response builders and their header manipulation
- Query parameter handling in redirect and location headers
- Template rendering with user-supplied data
You can scan your Axum API from the CLI:
npx middlebrick scan https://your-axum-api.com
Or integrate into your CI/CD pipeline:
name: API Security Scan
on: [push, pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run middleBrick scan
run: |
npx middlebrick scan https://staging.your-axum-api.com
The scan results include specific findings about CRLF injection vulnerabilities, with severity levels and remediation guidance tailored to Axum's architecture.
Axum-Specific Remediation
Securing Axum applications against CRLF injection requires input validation, proper encoding, and safe header construction patterns.
1. Input Validation and Sanitization:
use axum::http::HeaderValue;
use axum::response::IntoResponse;
fn sanitize_input(input: &str) -> String {
// Remove all CRLF characters
input.replace('
', "").replace('
', "")
}
async fn secure_handler(
Path(user_input): Path<String>,
) -> impl IntoResponse {
let sanitized = sanitize_input(&user_input);
let mut response = axum::response::Response::builder()
.header("X-User-Input", HeaderValue::from_str(&sanitized).unwrap())
.body(sanitized)
.unwrap();
response
}
2. Safe Header Construction:
use axum::http::HeaderValue;
use axum::response::IntoResponse;
async fn safe_redirect(
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
let user = params.get("user").unwrap_or(&"default".to_string());
// Validate URL before constructing
if !user.starts_with("https://allowed-domain.com/") {
return axum::response::Redirect::found("/error");
}
// Axum automatically handles proper encoding
axum::response::Redirect::found(user)
}
3. Using Axum's Built-in Safety Features:
use axum::extract::{Path, Query};
use axum::http::HeaderValue;
use axum::response::IntoResponse;
use serde::Deserialize;
#[derive(Deserialize)]
struct SafeParams {
user: String,
#[serde(default)]
tracking: String,
}
async fn secure_handler(
SafeParams { user, tracking }: SafeParams,
) -> impl IntoResponse {
// serde automatically handles percent-encoding
// Validate that user input doesn't contain CRLF
if user.contains('
') || user.contains('
') {
return (axum::http::StatusCode::BAD_REQUEST, "Invalid input");
}
let header_value = HeaderValue::from_str(&user).unwrap();
let mut response = axum::response::Response::builder()
.header("X-User-Input", header_value)
.body(format!("User: {}", user))
.unwrap();
response
}
4. Middleware for Global Protection:
use axum::middleware::Next;
use axum::response::IntoResponse;
use http::{Request, Response};
async fn crlf_protection(mut req: Request, next: Next) -> impl IntoResponse {
// Check for CRLF in headers
for (name, value) in req.headers().iter() {
if let Ok(value_str) = value.to_str() {
if value_str.contains('
') || value_str.contains('
') {
return (axum::http::StatusCode::BAD_REQUEST, "Invalid header");
}
}
}
next.run(req).await
}
// Apply middleware to router
let app = axum::Router::new()
.route("/api/secure/:input", get(secure_handler))
.layer(axum::middleware::from_fn(crlf_protection));