HIGH padding oracleaxumapi keys

Padding Oracle in Axum with Api Keys

Padding Oracle in Axum with Api Keys — how this specific combination creates or exposes the vulnerability

A padding oracle in an Axum service that uses API keys typically arises when the application decrypts request data (for example, an encrypted cookie, query parameter, or header value such as an API key) and reveals whether a padding error occurred during decryption. In a classic padding oracle, the server’s behavior differs depending on whether the padding is valid or invalid, often returning distinct HTTP status codes or messages for each case. If an Axum endpoint decrypts an API key or uses an encrypted API key as part of authorization and the decryption logic exposes padding errors, an attacker can iteratively submit modified ciphertexts and observe responses to infer the plaintext without needing to know the key.

In Axum, this often maps to a handler that receives an encrypted API key (for example, passed as a bearer token or a custom header), decrypts it using a symmetric cipher such as AES-CBC, and then branches logic based on whether the decrypted payload is valid. If the handler returns a 400 for bad padding and a 401 for invalid authentication, an attacker can use this side channel to recover the plaintext API key byte by byte. Even when API keys are expected to be high-entropy, the presence of a predictable padding scheme and a distinguishable error path turns the endpoint into an oracle.

Consider an endpoint that reads an encrypted API key from an Authorization header, base64-decodes it, and decrypts it using AES-CBC with PKCS7 padding. If decryption fails due to invalid padding, the handler may return one error; if padding is valid but the API key is not recognized, it returns another. This difference allows an attacker to mount a padding oracle attack by sending carefully crafted ciphertexts and observing responses. Even if the API key itself is not the secret, the decryption key and IV may be derivable, leading to compromise of the authorization mechanism. The risk is compounded when the same key is used across multiple endpoints or when the encrypted API key is reused across sessions.

Api Keys-Specific Remediation in Axum — concrete code fixes

Remediation focuses on ensuring that decryption failures do not leak information and that API key handling does not expose padding differences. In Axum, this means standardizing error responses for both padding and authentication failures, using constant-time comparison for any key material checks, and avoiding branching on padding validity.

First, use a cipher mode that provides authentication (such as AES-GCM or ChaCha20-Poly1305) so that decryption either succeeds fully or fails with a single generic error. If you must use AES-CBC, always apply HMAC-SHA256 (Encrypt-then-MAC) or use an authenticated encryption construction, and verify the MAC before attempting decryption. Ensure that any padding check is performed in a way that does not short-circuit on invalid bytes.

Second, ensure your Axum error handling returns the same HTTP status code and body shape for both padding errors and invalid API keys. This removes the oracle behavior. Below is an example of a hardened Axum handler that decrypts an encrypted API key from an Authorization header using AES-256-GCM, returning a consistent 401 response for any failure, and using constant-time comparison for the resolved key identifier.

use axum::{{
    async_trait,
    extract::{self, Request},
    http::StatusCode,
    response::IntoResponse,
    Extension,
}};
use aes_gcm::{{
    aead::{Aead, KeyInit, OsRng},
    Aes256Gcm, Nonce, Key, Payload,
}};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use std::collections::HashMap;

type HmacSha256 = Hmac;

async fn decrypt_api_key(encrypted_b64: &str, key: &[u8; 32]) -> Result {
    // Expect AES-256-GCM: decode, split nonce|ciphertext|tag
    let data = base64::decode(encrypted_b64).map_err(|_| "decrypt_error")?;
    if data.len() != 32 { // 12-byte nonce + 16-byte tag + at least 1 byte ciphertext
        return Err("decrypt_error");
    }
    let (nonce_bytes, ciphertext_and_tag) = data.split_at(12);
    let nonce = Nonce::from_slice(nonce_bytes);
    let cipher = Aes256Gcm::new(Key::from_slice(key));
    let payload = Payload { associated_data: &[], ciphertext: ciphertext_and_tag };
    let decrypted = cipher.decrypt(nonce, payload).map_err(|_| "decrypt_error")?;
    String::from_utf8(decrypted).map_err(|_| "decrypt_error")
}

async fn verify_hmac(key_id: &str, mac_b64: &str, secret: &[u8]) -> bool {
    let data = base64::decode(mac_b64).unwrap_or_default();
    let mut mac = HmacSha256::new_from_slice(secret).unwrap();
    mac.update(key_id.as_bytes());
    let computed = mac.finalize().into_bytes();
    if data.len() != computed.len() {
        return false;
    }
    computed.ct_eq(&data).into()
}

async fn get_api_key_from_request(req: &Request) -> Option {
    let auth = req.headers().get("authorization")?.to_str().ok()?;
    if let Some(token) = auth.strip_prefix("Bearer ") {
        Some(token.to_string())
    } else {
        None
    }
}

async fn handler(
    Extension(config): Extension>>,
    req: Request,
) -> Result {
    let encrypted = get_api_key_from_request(&req)
        .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?;
    let cfg = config.get("api_key")
        .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Server configuration error".to_string()))?
        .as_slice();
    // Assume cfg holds [key_32bytes, hmac_secret_32bytes]
    let key_32 = <[u8; 32]>::try_from(&cfg[..32]).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Server configuration error".to_string()))?;
    let hmac_secret = <[u8; 32]>::try_from(&cfg[32..]).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Server configuration error".to_string()))?;

    let api_key = decrypt_api_key(&encrypted, &key_32).await?;
    let valid = verify_hmac(&api_key, &encrypted, &hmac_secret);
    if !valid {
        return Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()));
    }
    // Proceed with routing or business logic using resolved api_key identifier
    Ok((StatusCode::OK, format!("Resolved key: {}", api_key)))
}

Frequently Asked Questions

Can a padding oracle leak an API key even if the key itself is not the secret?
Yes. If an endpoint decrypts an encrypted API key and distinguishes padding errors from bad authentication, an attacker can use the oracle to recover the plaintext key or the decryption parameters, undermining authorization.
Is using API keys in encrypted form sufficient to prevent padding oracle risks?
Not by itself. You must ensure decryption does not leak padding information, use authenticated encryption (e.g., AES-GCM), return uniform error responses, and avoid branching on padding validity.