HIGH timing attackactixbasic auth

Timing Attack in Actix with Basic Auth

Timing Attack in Actix with Basic Auth — how this specific combination creates or exposes the vulnerability

A timing attack in Actix when Basic Authentication is used occurs because the server’s comparison of the client-supplied credentials with the stored secret is not constant-time. In many implementations, the application computes a hash of the provided password and then compares it byte-by-byte with the stored hash using a standard equality check. In languages like Rust, methods such as == on byte slices or strings typically short-circuit: they return false immediately when the first mismatching byte is found. This means that an attacker can learn information about the correct hash one byte at a time by measuring how long each request takes.

Consider an Actix handler that manually decodes the Authorization: Basic base64(credentials) header, splits the decoded string into username:password, retrieves a user record, and then compares the provided password (after hashing) with the stored hash. Because the comparison is not constant-time, an attacker can send many requests with crafted passwords and observe response times. A slightly longer response time suggests that more bytes matched at the start of the hash. By repeating this process across bytes and characters, the attacker can gradually reconstruct the password hash without ever needing to authenticate successfully.

The risk is amplified when the endpoint does not enforce rate limiting or does so inconsistently. For example, if a global rate limiter is applied after authentication checks, an unauthenticated attacker can make many password guesses per second to probe timing differences. In an OpenAPI/Swagger-driven workflow, the unauthenticated attack surface includes the OPTIONS and discovery endpoints; scanning such an API with middleBrick can surface missing authentication on the login path and highlight inconsistent rate limiting that would otherwise enable timing-based enumeration.

In an Actix service, the vulnerability chain is: (1) non-constant-time credential comparison, (2) observable timing differences in HTTP response durations, and (3) sufficient request volume to extract meaningful signal. Even if TLS encrypts the traffic, the server-side timing behavior is observable to any network position able to measure request latency. This means that protections limited to transport security are insufficient; the application logic itself must ensure safe comparison patterns.

Basic Auth-Specific Remediation in Actix — concrete code fixes

To mitigate timing attacks in Actix with Basic Authentication, you must ensure that any comparison of secrets runs in constant-time and that authentication paths are guarded by uniform, predictable execution steps regardless of credential validity. Below are concrete, idiomatic Rust examples using Actix-web that address the issue.

Example 1: Constant-time password verification with a hashed store

Use a cryptographic hashing function that is appropriate for passwords (e.g., Argon2id) and compare hashes using a constant-time equality function. The subtle crate provides ConstantTimeEq for byte slices, which avoids early exit on mismatch.

use actix_web::{web, HttpRequest, HttpResponse, Result};
use base64::Engine;
use subtle::ConstantTimeEq;

async fn login_basic(req: HttpRequest, body: web::Bytes) -> Result {
    // Extract and decode the Authorization header
    let header = req.headers().get("authorization")
        .ok_or_else(|| actix_web::error::ErrorUnauthorized("Missing authorization"))?;
    let header_str = header.to_str().map_err(|_| actix_web::error::ErrorUnauthorized("Invalid header"))?;
    if !header_str.starts_with("Basic ") {
        return Err(actix_web::error::ErrorUnauthorized("Unsupported scheme"));
    }
    let encoded = &header_str[6..];
    let decoded = base64::engine::general_purpose::STANDARD.decode(encoded)
        .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid base64"))?;
    let credentials = std::str::from_utf8(&decoded)
        .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid utf8"))?;
    let (user, pass) = credentials.split_once(':')
        .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid credentials format"))?;

    // Retrieve stored hash and salt for the user (pseudo function)
    let (stored_hash, salt) = get_user_hash_and_salt(user).await;

    // Derive a hash from the provided password using the same parameters
    let attempt_hash = argon2::hash_encoded(pass.as_bytes(), &salt, &argon2::Config::default())
        .map_err(|_| actix_web::error::ErrorInternalServerError("Hashing error"))?;

    // Constant-time comparison of the encoded hash strings
    let stored = stored_hash.as_bytes();
    let attempt = attempt_hash.as_bytes();
    if stored.ct_eq(attempt).unwrap_u8() == 1u8 {
        Ok(HttpResponse::Ok().finish())
    } else {
        // Always consume similar time by performing a dummy hash and comparison
        let _dummy = argon2::hash_encoded(b"dummy", &salt, &argon2::Config::default())
            .unwrap_or_default();
        let _ = stored.ct_eq(_dummy.as_bytes()).unwrap_u8();
        Err(actix_web::error::ErrorUnauthorized("Invalid credentials"))
    }
}

Example 2: Uniform handling path with dummy lookups

When you cannot retrieve a stored hash without revealing whether a user exists, perform a dummy derivation for unknown users so that the execution time remains consistent. This prevents an attacker from distinguishing valid usernames based on response time.

async fn login_basic_uniform(req: HttpRequest) -> HttpResponse {
    let header = match req.headers().get("authorization") {
        Some(h) => h,
        None => return HttpResponse::Unauthorized().finish(),
    };
    let decoded = match decode_basic_header(header) {
        Ok((u, p)) => (u, p),
        Err(_) => return HttpResponse::Unauthorized().finish(),
    };

    // Always derive a hash, using a constant work factor and a deterministic fallback salt
    let salt = if known_user(&decoded.0) {
        fetch_salt(&decoded.0).await.unwrap_or_default()
    } else {
        // Use a fixed salt so the dummy work is comparable in cost
        b"fixed_salt_for_dummy"
    };
    let _stored = argon2::hash_encoded(b"placeholder", &salt, &argon2::Config::default())
        .unwrap_or_default();
    let attempt = argon2::hash_encoded(decoded.1.as_bytes(), &salt, &argon2::Config::default())
        .unwrap_or_default();

    // Compare with constant-time; in a real app you'd compare against stored hash
    if _stored.as_bytes().ct_eq(attempt.as_bytes()).unwrap_u8() == 1u8 {
        HttpResponse::Ok().finish()
    } else {
        // Perform a dummy comparison to keep timing uniform
        let dummy = argon2::hash_encoded(b"dummy", &salt, &argon2::Config::default())
            .unwrap_or_default();
        let _ = dummy.as_bytes().ct_eq(attempt.as_bytes()).unwrap_u8();
        HttpResponse::Unauthorized().finish()
    }
}

Operational protections

  • Apply consistent, global rate limiting before or alongside authentication logic so that an attacker cannot send an unbounded number of probes.
  • Use middleware that masks timing variance by introducing small, bounded jitter only when it does not interfere with correctness; prefer fixing the comparison logic as the primary defense.
  • Validate and test using tools that can detect timing side channels; treat any observable timing variance in authentication endpoints as a finding that requires remediation.

These changes ensure that the time taken to verify credentials does not depend on how many bytes of a secret are correct, thereby neutralizing timing-based extraction attacks against Basic Auth in Actix services.

Frequently Asked Questions

Why does constant-time comparison matter for Basic Auth in Actix?
Standard byte-by-byte equality checks short-circuit on the first mismatch, allowing an attacker to learn how many bytes of a password hash match by measuring request latency. Constant-time comparison ensures every comparison takes the same duration regardless of input, preventing information leakage through timing differences.
Can middleware that adds jitter fully replace constant-time comparisons?
No. Adding jitter can obscure timing signals but does not eliminate side channels; it may also affect performance and determinism. The primary fix is to use cryptographic constant-time equality for any secret comparison, combined with uniform handling paths for valid and invalid credentials.