Bleichenbacher Attack in Flask with Api Keys
Bleichenbacher Attack in Flask with Api Keys — how this specific combination creates or exposes the vulnerability
A Bleichenbacher attack is a cryptographic side-channel that exploits timing differences in RSA decryption to recover plaintext without the key. In Flask APIs that rely on API keys protected by RSA-based token validation, the combination of a Flask endpoint performing RSA decryption and an API key validation flow can create an oracle that a remote attacker can use to gradually reveal a valid signature or decrypt captured tokens.
Consider a Flask service that validates incoming API keys by verifying an RSA-signed JWT or PKCS#1 v1.5–style token. If the server returns different HTTP status codes or response times depending on whether a padding error occurs during RSA decryption, an attacker can iteratively send modified ciphertexts and observe responses. Over many requests, this adaptive chosen-ciphertext approach can recover the plaintext or forge a valid signature. This is the core of Bleichenbacher’s original attack against PKCS #1 v1.5 padding, later generalized to other asymmetric schemes where error handling leaks information.
When API keys are embedded inside such tokens, the attack surface expands. An attacker who can influence the token’s ciphertext—by tampering with an API key claim or observing timing differences across different keys—can learn whether a given key or padding is valid. In Flask, common triggers include using cryptography or PyCrypto with non-constant-time verification or returning 500 errors vs 401/403 distinctions that signal padding failures. Even unauthenticated endpoints that accept an API key parameter and perform RSA-based validation can act as an oracle if responses are not uniform in timing and content.
Because middleBrick tests unauthenticated attack surfaces, it can detect endpoints where RSA decryption is used in combination with API key validation and highlight timing-related anomalies or inconsistent error handling. Findings map to the Authentication and Input Validation checks, and remediation guidance is included in the scan report. Without fixes, an attacker may recover enough information to forge API keys or bypass intended access controls.
Api Keys-Specific Remediation in Flask — concrete code fixes
To mitigate Bleichenbacher-style side-channel attacks when using API keys with RSA-based validation in Flask, ensure cryptographic operations run in constant time and that error handling does not leak validation state. Below are concrete, secure patterns with working code examples.
First, prefer high-level APIs that handle padding and verification safely. When using the cryptography library, use padding.PKCS1v15 with hashes and always verify using a constant-time comparison. Do not branch on padding errors; instead, catch exceptions and return a uniform response.
from flask import Flask, request, jsonify
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.exceptions import InvalidSignature
import hmac
import secrets
app = Flask(__name__)
# Example: load public key safely
with open("public_key.pem", "rb") as f:
PUBLIC_KEY = serialization.load_pem_public_key(f.read())
def verify_signature(data: bytes, signature: bytes, key) -> bool:
try:
key.verify(
signature,
data,
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except InvalidSignature:
return False
# Do not catch other exceptions and reveal details
@app.route("/protected")
def protected():
api_key = request.args.get("api_key", "")
token = request.args.get("token", "")
if not api_key or not token:
return jsonify({"error": "missing credentials"}), 400
# Decode token safely; do not reveal which part failed
try:
data = token.encode("utf-8")
is_valid = verify_signature(data, token.encode("utf-8"), PUBLIC_KEY)
# Constant-time comparison for API key binding
expected = secrets.compare_digest(api_key.encode("utf-8"), b"known-safe-key")
if is_valid and expected:
return jsonify({"status": "ok"}), 200
except Exception:
# Generic error path to avoid information leakage
pass
return jsonify({"error": "invalid credentials"}), 403
if __name__ == "__main__":
app.run()
Second, avoid raw PKCS#1 v1.5 where possible; prefer RSAES-OAEP, which is not malleable and does not suffer from the same Bleichenbacher-like oracles. If you must use PKCS#1 v1.5, ensure decryption errors are caught and always take the same code path with identical timing characteristics.
from cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# OAEP decryption — safer against adaptive chosen-ciphertext attacks
def decrypt_oaep(ciphertext: bytes) -> bytes:
return private_key.decrypt(
ciphertext,
asym_padding.OAEP(
mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
Third, apply defense-in-depth: rate-limit endpoints, enforce short lifetimes for API keys, and avoid returning verbose errors that indicate whether an API key, signature, or padding failed. middleBrick’s Authentication and Input Validation checks can surface endpoints where timing inconsistencies or error differentiation exist, and its remediation guidance will point to these fixes.