Padding Oracle in Actix
How Padding Oracle Manifests in Actix
Padding Oracle attacks exploit the way cryptographic padding is handled during decryption. In Actix applications, this vulnerability typically manifests when using AES-CBC mode without proper error handling. The attack exploits the difference in timing or error messages between valid padding and invalid padding, allowing an attacker to decrypt ciphertext without knowing the encryption key.
Actix applications often handle encrypted cookies, JWT tokens, or encrypted request bodies. When decryption fails due to padding errors, the error handling must be uniform regardless of the failure reason. Many Actix implementations inadvertently leak information through HTTP status codes, response times, or error messages.
Here's a vulnerable Actix implementation that demonstrates the classic padding oracle pattern:
use actix_web::{web, App, HttpMessage, HttpResponse, HttpServer, Responder};
use actix_session::{CookieSession, Session};
use aes::Aes256Cbc;
use block_modes::BlockMode;
use hex_literal::hex;
async fn index(session: Session) -> impl Responder {
// Vulnerable: different error responses based on padding vs MAC failure
if let Ok(data) = session.get::("sensitive_data") {
HttpResponse::Ok().body(format!("Data: {}", data))
} else {
// This branch executes for padding errors, MAC errors, and missing data
// Attacker can distinguish padding failures from other failures
HttpResponse::BadRequest().body("Invalid session data")
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(
CookieSession::signed(&[0; 32])
.secure(false) // For development only
.http_only(true),
)
.service(web::resource("/").route(web::get().to(index)))
})
.bind("127.0.0.1:8080")?
.run()
.await
} The vulnerability appears when the application distinguishes between different types of decryption failures. An attacker can modify the encrypted cookie, observe whether the server returns a padding error versus a MAC error, and iteratively decrypt the payload character by character.
Another common pattern in Actix applications involves custom middleware that handles encrypted request bodies:
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
struct EncryptedPayload(Vec);
impl FromRequest for EncryptedPayload {
type Config = ();
type Error = Error;
type Future = Ready>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
// Vulnerable: timing differences between padding validation and decryption
let mut body = actix_web::body::to_bytes(payload).await.unwrap();
let decrypted = decrypt_aes_cbc(&body).unwrap_or_else(|e| {
// Different execution paths for different error types
if e.is_padding_error() {
// Faster path - returns immediately
return vec![];
} else {
// Slower path - additional validation
return vec![];
}
});
ready(Ok(EncryptedPayload(decrypted)))
}
} The timing differences between padding validation and other cryptographic operations create a side-channel that skilled attackers can exploit to recover plaintext without the encryption key.
Actix-Specific Detection
Detecting padding oracle vulnerabilities in Actix applications requires both static analysis and dynamic testing. The dynamic approach involves sending modified ciphertext and observing the server's response characteristics.
middleBrick's black-box scanning approach is particularly effective for Actix applications because it tests the actual runtime behavior without requiring source code access. The scanner sends specifically crafted requests that trigger padding validation and measures response characteristics.
Here's how middleBrick detects padding oracle vulnerabilities in Actix applications:
use actix_web::{test, App};
use std::time::Instant;
async fn detect_padding_oracle(app: &test::TestApp) {
// Step 1: Capture a valid encrypted payload
let valid_response = app.get("/protected").send().await;
let encrypted_cookie = valid_response.headers().get("set-cookie").unwrap();
// Step 2: Modify the last byte of the ciphertext
let mut modified_cookie = encrypted_cookie.clone();
modified_cookie[modified_cookie.len() - 1] ^= 0x01;
// Step 3: Measure response time and status code
let start = Instant::now();
let modified_response = app
.get("/protected")
.send()
.await;
let duration = start.elapsed();
// Step 4: Analyze response characteristics
let is_padding_error = modified_response.status() == 400 && duration.as_millis() < 100;
let is_other_error = modified_response.status() == 500 || duration.as_millis() > 500;
if is_padding_error {
println!("Potential padding oracle detected - timing analysis shows padding validation");
}
}
middleBrick automates this entire process across multiple endpoints and payload types. It tests cookies, headers, and request bodies that might contain encrypted data. The scanner maintains a database of known padding oracle patterns and compares response characteristics against these baselines.
For Actix applications using JWT tokens, middleBrick specifically tests for timing differences between signature verification failures and payload decryption failures:
async fn test_jwt_padding_oracle(app: &test::TestApp) {
// Test valid JWT
let valid_jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
let valid_response = app
.get("/api/data")
.append_header(("Authorization", format!("Bearer {}", valid_jwt)))
.send()
.await;
// Test modified JWT with padding changes
let mut jwt_parts: Vec<&str> = valid_jwt.split('.').collect();
let mut payload = base64::decode(jwt_parts[1]).unwrap();
payload[payload.len() - 1] ^= 0x01;
jwt_parts[1] = base64::encode(&payload);
let modified_jwt = jwt_parts.join(".");
let start = Instant::now();
let modified_response = app
.get("/api/data")
.append_header(("Authorization", format!("Bearer {}", modified_jwt)))
.send()
.await;
let duration = start.elapsed();
// middleBrick flags if timing difference exceeds threshold
if duration.as_millis() < 100 && modified_response.status() == 400 {
println!("JWT padding oracle candidate detected");
}
}The scanner also checks for error message consistency, ensuring that the same error message is returned regardless of whether the failure is due to padding, MAC verification, or other cryptographic operations.
Actix-Specific Remediation
Remediating padding oracle vulnerabilities in Actix applications requires eliminating all information leakage through uniform error handling and constant-time cryptographic operations. The key principle is that all decryption failures must be handled identically, regardless of the underlying cause.
Here's a secure Actix implementation that prevents padding oracle attacks:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use aes::Aes256Cbc;
use block_modes::{BlockMode, BlockModeError};
use constant_time_eq::constant_time_eq;
use hmac::{Hmac, Mac};
use rand::Rng;
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
// Secure middleware that handles decryption uniformly
struct SecureCryptoMiddleware;
impl SecureCryptoMiddleware {
async fn decrypt_uniform(data: &[u8]) -> Result {
// Constant-time decryption with uniform error handling
let result = aes256_cbc_decrypt(data);
// Always perform the same operations regardless of success/failure
let dummy_operations = self::perform_dummy_operations(data);
// Use constant-time comparison to prevent timing attacks
let success = result.is_ok() && constant_time_eq(&dummy_operations, &dummy_operations);
if success {
result
} else {
// Return generic error without revealing failure type
Err(BlockModeError::new("Decryption failed"))
}
}
fn perform_dummy_operations(data: &[u8]) -> Vec<u8> {
// Dummy operations that take constant time
let mut rng = rand::thread_rng();
let mut dummy = vec![0u8; data.len()];
rng.fill(&mut dummy);
dummy
}
}
async fn secure_endpoint(
web::Path(id): web::Path<String>,
crypto: web::Data<SecureCryptoMiddleware>,
) -> impl Responder {
// Uniform handling - no information leakage
let decrypted = match crypto.decrypt_uniform(id.as_bytes()).await {
Ok(data) => data,
Err(_) => {
// Always return the same response
return HttpResponse::BadRequest()
.body("Invalid request data");
}
};
// Process decrypted data
HttpResponse::Ok().body(format!("Processed: {:?}", decrypted))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.app_data(web::Data::new(SecureCryptoMiddleware))
.service(web::resource("/secure/{id}").route(web::post().to(secure_endpoint)))
})
.bind("127.0.0.1:8080")?
.run()
.await
} For JWT handling in Actix, use a secure validation approach that doesn't distinguish between different failure types:
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use jsonwebtoken::{decode, Validation, DecodingKey};
use time::OffsetDateTime;
use std::future::ready;
use futures_util::future::Ready;
struct SecureJwtPayload;
impl FromRequest for SecureJwtPayload {
type Config = ();
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let token = match req.headers().get("authorization") {
Some(header) => header.to_string(),
None => return ready(Err(Error::from(()))),
};
// Uniform validation - same time regardless of failure type
let start = std::time::Instant::now();
let result = validate_jwt_uniform(token);
let duration = start.elapsed();
// Add constant delay to prevent timing analysis
if duration.as_millis() < 100 {
std::thread::sleep(std::time::Duration::from_millis(100 - duration.as_millis() as u64));
}
match result {
Ok(_) => ready(Ok(SecureJwtPayload)),
Err(_) => ready(Err(Error::from(()))),
}
}
}
fn validate_jwt_uniform(token: String) -> Result<(), jsonwebtoken::errors::Error> {
// Dummy operations to ensure constant time
let mut dummy = [0u8; 32];
for i in 0..32 {
dummy[i] = (i as u8).wrapping_add(token.as_bytes()[i % token.len()]);
}
// Actual validation
let token_data = match decode::(&token,
&DecodingKey::from_secret("secret".as_bytes()),
&Validation::new(jsonwebtoken::Algorithm::HS256)) {
Ok(data) => data,
Err(e) => return Err(e.into()),
};
// Verify claims uniformly
let now = OffsetDateTime::now_utc();
let is_valid = token_data.claims.exp.unwrap_or(now) > now &&
token_data.claims.nbf.unwrap_or(now) < now;
if is_valid {
Ok(())
} else {
Err(jsonwebtoken::errors::Error::new("Invalid claims"))
}
}
The middleBrick CLI tool can help verify your remediation:
# Scan your Actix application after remediation
middlebrick scan http://localhost:8080 --api --jwt --cookies
# Run in CI/CD to ensure no regression
middlebrick scan http://staging-api:8080 --fail-below B
middleBrick's continuous monitoring (Pro plan) can automatically re-scan your API endpoints on a schedule, ensuring that padding oracle vulnerabilities don't reappear through code changes or dependency updates.