Padding Oracle in Axum with Basic Auth
Padding Oracle in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
A padding oracle in an Axum service that uses HTTP Basic Authentication arises when two conditions intersect: (1) the server performs decryption or validation of ciphertext before checking whether the caller is authorized for the requested resource, and (2) different error behaviors or status codes are returned depending on whether a padding error occurs versus an authentication or authorization failure. In Axum, this can happen when encrypted request bodies (e.g., a JWT or custom encrypted blob) are processed eagerly, and a padding validation failure produces a distinct 400 response, while an authenticated but unauthorized request yields 403. An attacker who can intercept or observe these status-code differences can iteratively decrypt data or recover plaintext without knowing the key, treating the API as a padding oracle.
Consider an endpoint that decrypts a ciphertext from an Authorization header or request body, then validates credentials. If Axum returns 400 for bad padding and 401 for bad credentials, an attacker can craft ciphertexts that reveal information about the plaintext through timing and status-code feedback. This is especially risky when Basic Auth credentials are encrypted before processing: the server may decrypt, validate padding, and then check credentials. The distinction between a padding error and a credential error becomes an oracle. Even when credentials are sent in the Authorization header as base64-encoded plaintext (not encrypted), if the application decrypts some other payload and uses timing or error paths that differ based on padding correctness, the combination still exposes the oracle.
Real-world attack patterns often exploit this in APIs that use custom encryption or legacy protocols where padding schemes such as PKCS#7 are not validated with constant-time logic. For example, if an Axum handler deserializes a JSON body containing an encrypted field, decrypts it using a block cipher in CBC mode without proper padding checks, and then conditionally returns 400 versus 401, it inadvertently implements a padding oracle. Attackers can leverage this to recover session tokens or other sensitive data by sending modified ciphertexts and observing responses. OWASP API Security Top 10 categories such as Broken Object Level Authorization (BOLA) and Security Misconfiguration intersect here when authorization checks occur after decryption and when error handling leaks implementation details.
Middleware choices in Axum can inadvertently create these distinctions. If you log or return different HTTP statuses depending on where in the processing chain an error occurs, and if one path involves cryptographic operations, you may create an observable difference. For instance, failing early due to a malformed ciphertext with one status, versus failing after decryption due to invalid padding with another, provides an exploitable signal. Even without explicitly returning errors, timing differences in cryptographic validation can be measurable and useful to a network attacker.
To mitigate this specific combination, ensure that all decryption and validation paths produce uniform error handling and status codes regardless of whether the issue is padding, authentication, or authorization. In Axum, this means designing extractors and guards so that cryptographic failures do not short-circuit with distinct responses before authorization checks. Combine this with constant-time comparison routines for any sensitive validation logic and avoid exposing processing stage distinctions to the client. Tools like middleBrick can help detect whether your unauthenticated endpoints exhibit status-code differences that resemble a padding oracle, by analyzing runtime behavior against spec-defined error paths.
Basic Auth-Specific Remediation in Axum — concrete code fixes
Remediation focuses on removing the oracle by ensuring that authentication and authorization happen before any cryptographic processing when possible, and that error handling is consistent. If you must process encrypted payloads, perform padding validation in a constant-time manner and return a single generic error status for all client-side failures. Below are concrete Axum examples that demonstrate secure handling with Basic Auth.
First, a secure approach where credentials are validated before any decryption, avoiding the need to decrypt attacker-controlled ciphertexts when authorization would fail. This eliminates the scenario where padding errors are observable before authentication.
use axum::{routing::get, Router, extract::Request, http::HeaderMap};
use tower_http::auth::{AuthLayer, Authorization};
use tower_http::auth::authorization::AuthorizationHeader;
async fn handler() -> &'static str {
"ok"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/secure", get(handler))
.layer(AuthLayer::bearer_service::());
// or for Basic Auth:
let basic_layer = tower_http::auth::AuthLayer::basic_service::();
let app = Router::new()
.route("/secure", get(handler))
.layer(basic_layer);
}
struct MyBasicValidator;
impl tower_http::auth::ValidateRequest for MyBasicValidator {
type Rejection = axum::http::StatusCode;
fn validate_request(
&self,
req: &Request,
credentials: AuthorizationHeader,
) -> Result, Self::Rejection> {
let creds = credentials.into_parts();
let userpass = creds.decode_utf8().map_err(|_| axum::http::StatusCode::UNAUTHORIZED)?;
// Perform constant-time comparison for username/password
if validate_user_pass_constant_time(userpass.username(), userpass.password()) {
Ok(Authorization::default())
} else {
Err(axum::http::StatusCode::UNAUTHORIZED)
}
}
}
fn validate_user_pass_constant_time(user: &str, pass: &str) -> bool {
// Use a constant-time comparison for secrets to avoid timing leaks
// Example using subtle::ConstantTimeEq; here we show a simplified placeholder
let expected_user = "admin";
let expected_pass = "s3cr3t";
subtle::ConstantTimeEq::ct_eq(user.as_bytes(), expected_user.as_bytes()).into()
& subtle::ConstantTimeEq::ct_eq(pass.as_bytes(), expected_pass.as_bytes()).into()
== 1
}
If you accept encrypted payloads, ensure decryption errors do not leak padding status. Use AEAD modes where possible, or ensure padding validation is performed in constant time and that failures result in the same status regardless of cause.
use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct EncryptedPayload {
ciphertext: Vec,
}
#[derive(Serialize)]
struct EmptyResponse;
async fn handle_encrypted(Json(payload): Json) -> Result, axum::http::StatusCode> {
// Decrypt with constant-time padding validation; on any failure return 400 uniformly
let plaintext = decrypt_constant_time(&payload.ciphertext).map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
// Perform auth checks after decryption; if unauthorized, still return 400 to avoid oracle
if !is_authorized(&plaintext) {
return Err(axum::http::StatusCode::BAD_REQUEST);
}
Ok(Json(EmptyResponse))
}
fn decrypt_constant_time(ciphertext: &[u8]) -> Result, CryptoError> {
// Placeholder: use your crypto library with constant-time padding checks
unimplemented!()
}
fn is_authorized(plaintext: &[u8]) -> bool {
// Authorization logic
false
}
These examples illustrate how to structure Axum handlers so that Basic Auth and decryption errors do not produce distinguishable responses. By using the tower_http auth layer and ensuring uniform error handling, you remove the conditions that enable a padding oracle. middleBrick’s scans can verify that your endpoints do not expose status-code differences across authentication and cryptographic failure paths.