Header Injection in Axum with Cockroachdb
Header Injection in Axum with Cockroachdb — how this specific combination creates or exposes the vulnerability
Header Injection occurs when user-controlled data is reflected into HTTP response headers without validation or sanitization. In an Axum application using Cockroachdb as the backend datastore, the risk emerges from how headers are composed from database values and how Axum’s extractor model interacts with response construction.
Consider an endpoint that fetches a user profile from Cockroachdb and sets a custom header such as x-user-role or x-tenant-id based on a column value. If the application does not treat these database fields as untrusted, an attacker who can influence the stored data (for example, via an upstream admin interface or a compromised service account) can inject newline characters (CRLF, %0d%0a or literal \r\n) into header-valued columns. When Axum builds the response, these injected sequences can cause header splitting, leading to response splitting, HTTP response smuggling, or the insertion of additional headers like Set-Cookie or location redirects.
Axum does not automatically sanitize data taken from SQL rows before placing them into headers. If you extract a field with sqlx or an ORM and pass it directly to HeaderMap::insert, you effectively bypass any separation between protocol layer and application data. Cockroachdb, being PostgreSQL-wire compatible, does not prevent newline characters in text columns; it stores and returns them as-is. Therefore, the server-side code must enforce strict allow-listing and encoding before using any data-derived value as a header.
Another vector specific to this stack is reflected header-based authentication or tracing. An endpoint might read a request header like X-Request-ID, query Cockroachdb to correlate logs or sessions, and then echo that identifier in a response header such as x-request-id. If the stored identifier contains CRLF sequences, the echoed header can break header structure. MiddleBrick’s scans detect such combinations by correlating OpenAPI spec definitions with runtime response headers and flagging user-controlled data flows that reach headers without validation.
Example of a vulnerable Axum handler:
async fn profile_handler(
user_id: Path,
pool: &State>,
) -> Result {
let row = pool
.fetch_one(&format!("SELECT role FROM profiles WHERE id = {}", user_id))
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let role: String = row.get(0);
let mut resp = Response::builder()
.header("x-user-role", role) // Unsafe: role from Cockroachdb
.body(Body::empty())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(resp)
}
If the role column contains admin\r\nSet-Cookie: session=steal, the response can inject an additional header. Axum’s extractor model does not raise an error here, but the resulting response is unsafe.
Cockroachdb-Specific Remediation in Axum — concrete code fixes
Remediation centers on treating all database-derived values as untrusted and applying strict validation before they are placed into headers. Below are concrete Axum patterns with Cockroachdb-backed code examples that prevent header injection.
1. Reject or encode unsafe characters
Before inserting a value into a header, strip or percent-enable CRLF and other control characters. Use a dedicated sanitization function that operates on strings extracted from Cockroachdb rows.
fn safe_header_value(value: &str) -> String {
value.chars().filter(|c| !c.is_control()).collect()
}
async fn profile_handler_safe(
user_id: Path,
pool: &State>,
) -> Result {
let row = pool
.fetch_one(&format!("SELECT role FROM profiles WHERE id = {}", user_id))
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let role: String = row.get(0);
let safe_role = safe_header_value(&role);
let resp = Response::builder()
.header("x-user-role", safe_role)
.body(Body::empty())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(resp)
}
2. Use typed, allow-listed values
Instead of storing free-text roles in Cockroachdb, use enumerated types or reference tables and map them to safe strings in Axum. This ensures that only known-good values reach the header layer.
#[derive(sqlx::Type, Debug, Clone, PartialEq, Eq)]
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
enum UserRole {
User,
Admin,
Guest,
}
async fn profile_handler_enum(
user_id: Path,
pool: &State>,
) -> Result {
let role_enum: UserRole = pool
.fetch_one(&format!("SELECT role FROM profiles WHERE id = {}", user_id))
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let role_str = match role_enum {
UserRole::Admin => "admin",
UserRole::User => "user",
UserRole::Guest => "guest",
};
let resp = Response::builder()
.header("x-user-role", role_str)
.body(Body::empty())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(resp)
}
3. Validate input that influences header names
Never allow user input to dictate header names. If you must dynamically set headers, maintain a static mapping and validate against it.
async fn custom_header_handler(
user_id: Path,
pool: &State>,
header_key: String,
) -> Result {
// Static allow-list for dynamic header keys
let allowed = ["x-tenant-id", "x-correlation-id"];
if !allowed.contains(&header_key.as_str()) {
return Err((StatusCode::BAD_REQUEST, "Invalid header key".into()));
}
let value: String = pool
.fetch_one(&format!("SELECT meta->>'{}' FROM tenants WHERE id = {}", header_key, user_id))
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let safe_value = safe_header_value(&value);
let resp = Response::builder()
.header(header_key, safe_value)
.body(Body::empty())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(resp)
}
These patterns align with OWASP API Security Top 10 risks such as injection and broken function level authorization. MiddleBrick can identify endpoints where database-derived values reach headers without validation by correlating spec definitions with runtime behavior, helping teams prioritize fixes.