Xml External Entities in Axum with Basic Auth
Xml External Entities in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
XML External Entity (XXE) injection occurs when an XML parser processes external entity references in a way that can disclose local files, trigger SSRF, or consume system resources. In Axum-based services that accept XML payloads, the risk is elevated when authentication is handled via HTTP Basic Auth and the endpoint trusts user-supplied XML without disabling external entity resolution.
Basic Auth itself does not parse XML, but it often guards an endpoint that deserializes XML bodies. If the XML parser is configured to load external DTDs or parameter entities (e.g., with features.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) disabled in a non-secure way), an attacker can embed malicious external references. A typical vulnerable Axum handler might look like this, where Basic Auth is checked but XML parsing is permissive:
use axum::{
body::Body,
extract::Request,
http::{self, Request as HttpRequest, StatusCode},
response::Response,
routing::post,
Router,
};
use std::convert::Infallible;
use hyper::Body as HyperBody;
async fn auth_middleware(
request: Request,
credentials: (String, String), // username, password from Basic Auth header
) -> Result {
// naive Basic Auth check
if credentials.0 == "admin" && credentials.1 == "secret" {
Ok(request)
} else {
Err((StatusCode::UNAUTHORIZED, "Invalid credentials".into()))
}
}
async fn parse_xml(body: HyperBody) -> Result {
let bytes = hyper::body::to_bytes(body).await.map_err(|e| e.to_string())?;
let doc = roxmltree::Document::parse(&bytes).map_err(|e| e.to_string())?;
// Dangerous: no prevention of external entities
Ok(doc.root().text().unwrap_or("").to_string())
}
async fn handler(
auth: Option<(&str, &str)>, // extracted Basic Auth
body: hyper::Body,
) -> Result {
let credentials = auth.ok_or((StatusCode::UNAUTHORIZED, "Missing auth".into()))?;
auth_middleware(Request::new(Body::empty()), credentials.into()).await?;
let result = parse_xml(body).await?;
Ok(Response::new(Body::from(result)))
}
fn main() {
let app = Router::new()
.route("/api/xml", post(handler));
// run omitted
}
In this setup, an attacker authenticated with valid Basic Auth credentials can submit an XML body such as:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>>
<foo>&xxe;</foo>
If the XML parser resolves the external entity, the server may read /etc/passwd and include its contents in the response or in logs. Even when the endpoint requires Basic Auth, the combination means an attacker who obtains or guesses a valid credential pair can weaponize XXE to move laterally within the environment. This exposes file paths, internal service metadata, and potentially secrets mounted in the container filesystem. Moreover, if the Axum service is reachable internally (e.g., within a Kubernetes pod), the same XXE vector can be used for SSRF against internal endpoints that do not expect external traffic.
Additional risks include billion laughs attacks and entity expansion when external DTDs are resolved, leading to denial of service. Because Axum does not provide built-in XML parsing, the responsibility shifts to the developer to configure the underlying parser securely. Even with Basic Auth enforcing identity, the unchecked XML deserialization remains a critical path for data exposure and server-side request forgery.
Basic Auth-Specific Remediation in Axum — concrete code fixes
Remediation focuses on two layers: strict authentication practices and secure XML parsing. For Basic Auth, avoid hardcoded credentials and prefer opaque tokens or integrating with a proper identity provider. When Basic Auth is required, validate credentials against a secure store and enforce transport security (TLS). For XML handling, disable external entity processing and DTDs entirely.
Below is a hardened Axum handler that combines safe Basic Auth verification and a secure XML parser configuration using the roxmltree crate, which does not resolve external entities by default. This reduces the attack surface significantly:
use axum::{
body::Body,
extract::Request,
http::{self, header::AUTHORIZATION, Request as HttpRequest, StatusCode, HeaderMap, HeaderValue},
response::Response,
routing::post,
Router,
};
use base64::Engine;
use roxmltree::Document;
use std::convert::Infallible;
use hyper::{Body as HyperBody, Request as HyperRequest};
const VALID_USER: &str = "admin";
const VALID_PASS: &str = "correct-horse-battery-staple"; // use env/secrets in production
fn validate_basic_auth(headers: &HeaderMap) -> Result<(String, String), (StatusCode, String)> {
headers.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Basic "))
.and_then(|encoded| base64::engine::general_purpose::STANDARD.decode(encoded).ok())
.and_then(|decoded| String::from_utf8(decoded).ok())
.and_then(|cred| {
let mut parts = cred.splitn(2, ':');
match (parts.next(), parts.next()) {
(Some(u), Some(p)) if u == VALID_USER && p == VALID_PASS =
Ok((u.to_string(), p.to_string())),
_ => Err(()),
}
})
.ok_or_else(|| (StatusCode::UNAUTHORIZED, "Invalid Basic Auth".into()))
}
fn secure_parse_xml(bytes: &[u8]) -> Result<String, (StatusCode, String)> {
// roxmltree does not resolve external entities; safe by default
let doc = Document::parse(bytes).map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
// Normalize and extract only text nodes; avoid DTD/entity expansion
Ok(doc.root().text().unwrap_or("").to_string())
}
async fn handler(
headers: HeaderMap,
body: HyperBody,
) -> Result<Response<Body>, (StatusCode, String)> {
let credentials = validate_basic_auth(&headers)?;
let body_bytes = hyper::body::to_bytes(body).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let text = secure_parse_xml(&body_bytes)?;
Ok(Response::new(Body::from(text)))
}
fn main() {
let app = Router::new()
.route("/api/xml", post(handler));
// run with axum::Server omitted
}
Key remediation points:
- Always use TLS to prevent Basic Auth credentials from being intercepted.
- Validate credentials against a strong secret store rather than hardcoding them in production.
- Use XML libraries that do not process external entities by default (e.g.,
roxmltreein Rust) or explicitly disable DTD and external entity resolution if using other parsers. - Reject XML content types that include inline DOCTYPE declarations when not strictly required.
- Apply the same secure parsing logic to JSON and other formats if your API accepts multiple serialization types.
By combining authenticated access with a parser configuration that disallows external entity resolution, you mitigate XXE while preserving legitimate XML processing needs.