Spring4shell in Axum with Basic Auth
Spring4shell in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
Spring4shell (CVE-2022-22965) exploits a flaw in Spring MVC’s data binding when controllers use specific classpath conditions, typically involving @RequestParam or @RequestBody binding to objects like SimpleEvaluationContext. In an Axum service that uses Basic Auth for access control, the presence of authentication headers does not reduce the attack surface if the endpoint is reachable and reflects user-controlled input. Axum handlers often forward parsed request data into business logic or into structures that Spring may attempt to bind, which can trigger the gadget chain used for remote code execution.
The risk is compounded when Basic Auth is implemented naively: if the handler validates credentials but then passes the raw payload directly into a controller method expecting a mutable object, the deserialization path remains open. An attacker can send a crafted Content-Type header and payload that leverages Spring’s expression language to execute arbitrary code, even when a 401 would be expected for invalid credentials. middleBrick scans this combination by testing unauthenticated endpoints and checking for parameter pollution, object injection indicators, and exposed controller signatures that align with the Spring4shell gadget chain.
During a scan, middleBrick’s checks for Input Validation, BOLA/IDOR, and Property Authorization highlight whether user data is bound too permissively. The LLM/AI Security module additionally probes for server-side template injection or prompt-like injection vectors if any part of the request influences downstream language model calls. Because Axum routes often map cleanly to controller functions, a misconfigured binder can map directly to a vulnerable path, making runtime verification essential even when Basic Auth is in place.
Basic Auth-Specific Remediation in Axum — concrete code fixes
Secure Basic Auth in Axum requires strict validation, avoiding direct binding of raw request bodies to domain models, and explicitly rejecting unexpected content types. Prefer extracting credentials from headers and validating them before any deserialization occurs. Use extractor combinators to short-circuit unauthorized requests and ensure the request payload is only bound after successful authentication.
Below is a concrete, working Axum example that demonstrates safe handling of Basic Auth without exposing the application to Spring4shell-style gadget chains via parameter pollution or unsafe binding.
use axum::{
async_trait,
extract::{self, FromRequest, Request},
http::{header, StatusCode},
response::IntoResponse,
routing::get, Router,
};
use base64::prelude::*;
use std::{convert::Infallible, net::SocketAddr};
#[derive(Debug, Clone)]
struct AuthenticatedUser {
username: String,
}
struct BasicAuth {
username: String,
password: String,
}
#[async_trait]
impl FromRequest<S> for AuthenticatedUser
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let auth_header = req.headers().get(header::AUTHORIZATION)
.ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header"))?;
let auth_str = auth_header.to_str().map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid header encoding"))?;
if !auth_str.starts_with("Basic ") {
return Err((StatusCode::UNAUTHORIZED, "Invalid authorization scheme"));
}
let base64 = &auth_str[6..];
let decoded = BASE64_STANDARD.decode(base64).map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid base64"))?;
let credentials = String::from_utf8(decoded).map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid credentials encoding"))?;
let parts: Vec<&str> = credentials.splitn(2, ':').collect();
if parts.len() != 2 {
return Err((StatusCode::UNAUTHORIZED, "Invalid credentials format"));
}
// Validate against a secure store; avoid binding request body directly here
if parts[0] == "admin" && parts[1] == "secret" {
Ok(AuthenticatedUser { username: parts[0].to_string() })
} else {
Err((StatusCode::UNAUTHORIZED, "Bad credentials"))
}
}
}
async fn protected() -> impl IntoResponse {
"Authenticated access granted"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/secure", get(protected))
.layer(extract::DefaultBodyConfig::new().limit(256));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Key practices demonstrated:
- Credentials are extracted and validated in a custom extractor, preventing the request body from being automatically bound to command objects.
- No dynamic evaluation or expression language is invoked based on user input, avoiding gadget chain triggers.
- The request body is not deserialized into mutable structures that could be influenced by parameter pollution; if you need to accept JSON, validate and map it to immutable DTOs after authentication.
Additionally, configure global rejection limits and disable unwanted content types to reduce the likelihood of smuggling attacks that could bypass the Basic Auth layer.