Padding Oracle in Flask with Bearer Tokens
Padding Oracle in Flask with Bearer Tokens — how this specific combination creates or exposes the vulnerability
A padding oracle attack can occur in Flask when an application decrypts token payloads and uses error timing or behavior to infer whether padding is valid. This is especially relevant when Bearer Tokens are used to carry encrypted data, such as a JWT or a custom encrypted blob, and the server performs decryption server-side without authenticated encryption or constant-time checks.
In Flask, a typical vulnerable pattern is to read a Bearer Token from the Authorization header, extract the ciphertext, and decrypt it using a block cipher (e.g., AES-CBC) without proper integrity verification. If the decryption call raises different exceptions for invalid padding versus invalid MAC or other errors, an attacker can observe timing differences or HTTP status codes to act as an oracle. For example, Flask routes that return 401 for bad signature and 400 for bad padding can be exploited to gradually decrypt the token or forge valid tokens by iteratively querying the endpoint with modified ciphertexts.
Consider this simplified Flask route:
from flask import Flask, request, jsonify
from Crypto.Cipher import AES
from base64 import b64decode
import os
app = Flask(__name__)
KEY = os.urandom(32) # Insecure key management in practice
def decrypt_data(ciphertext_b64):
iv = ciphertext_b64[:16]
ciphertext = ciphertext_b64[16:]
cipher = AES.new(KEY, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
# Simulate padding removal that may raise ValueError on bad padding
if plaintext[-1] > 16:
raise ValueError("Invalid padding")
return plaintext.rstrip(b'\x00')
@app.route('/api/me')
def me():
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return jsonify(error='Unauthorized'), 401
token = auth.split(' ')[1]
try:
data = decrypt_data(b64decode(token))
return jsonify(user=data.decode())
except ValueError as e:
return jsonify(error=str(e)), 400
except Exception:
return jsonify(error='invalid_token'), 401
if __name__ == '__main__':
app.run(debug=False)
In this example, an attacker can send modified Bearer Tokens and observe whether the server responds with 400 (padding error) or 401/other (decryption or signature failure). Over many requests, this behavior can be used to decrypt the token or create valid ciphertexts without knowing the key. The vulnerability is not specific to Bearer Tokens per se, but using Bearer Tokens to transport encrypted payloads over HTTPS without additional integrity protection (e.g., an HMAC or use of an authenticated mode like AES-GCM) makes the token a target for padding oracle attacks.
Additionally, if token introspection or resource loading depends on decrypted claims, flawed error handling may inadvertently expose whether a ciphertext decrypts successfully, reinforcing the oracle. MiddleBrick scans detect such runtime behaviors by correlating spec-defined authentication schemes (Bearer) with observed error patterns and unauthenticated attack surface tests, highlighting places where decryption endpoints act as padding oracles.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring that decryption and validation fail consistently and do not reveal padding or validity information via timing or status codes. Use authenticated encryption so that integrity is verified before any decryption logic runs, and handle all failures with the same generic error response and similar timing characteristics.
Recommended approach: prefer token formats that include integrity by default (e.g., JWT signed with RS25/ES256) or use AES-GCM for encryption. If you must use AES-CBC, add an HMAC over the ciphertext and verify the HMAC before decryption. Always use constant-time comparison for MAC verification and ensure exceptions do not distinguish between padding failures and other errors.
Here is a hardened Flask example using AES-GCM (authenticated encryption), which avoids padding entirely:
from flask import Flask, request, jsonify
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
import os
app = Flask(__name__)
KEY = os.urandom(32)
def decrypt_data_gcm(token_b64):
data = b64decode(token_b64)
nonce = data[:12]
ciphertext = data[12:-16]
tag = data[-16:]
cipher = AES.new(KEY, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
return plaintext
@app.route('/api/me')
def me():
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return jsonify(error='Unauthorized'), 401
token = auth.split(' ')[1]
try:
data = decrypt_data_gcm(token)
return jsonify(user=data.decode())
except (ValueError, KeyError):
# Use a generic response and status code to avoid leaking info
return jsonify(error='invalid_token'), 401
if __name__ == '__main__':
app.run(debug=False)
If you must use CBC, verify an HMAC in constant time before decryption:
import hashlib
import hmac
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
def verify_and_decrypt_cbc(token_b64, key, hmac_key):
data = b64decode(token_b64)
iv = data[:16]
ciphertext = data[16:-32]
received_mac = data[-32:]
# Constant-time MAC verification
expected_mac = hmac.new(hmac_key, iv + ciphertext, hashlib.sha256).digest()
if not hmac.compare_digest(expected_mac, received_mac):
raise ValueError("Bad MAC")
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
# Standard PKCS7 removal (do not reveal padding errors)
pad_len = plaintext[-1]
if pad_len < 1 or pad_len > 16:
raise ValueError("Bad padding")
plaintext = plaintext[:-pad_len]
return plaintext
app = Flask(__name__)
KEY = os.urandom(32)
HMAC_KEY = os.urandom(32)
@app.route('/api/me')
def me():
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return jsonify(error='Unauthorized'), 401
token = auth.split(' ')[1]
try:
data = verify_and_decrypt_cbc(token, KEY, HMAC_KEY)
return jsonify(user=data.decode())
except ValueError:
return jsonify(error='invalid_token'), 401
Additional remediation guidance:
- Never rely on separate endpoints or status codes to indicate padding versus MAC failures; unify responses.
- Use frameworks or libraries that provide authenticated encryption by default (e.g., PyJWT with asymmetric keys).
- Rotate keys and re-encrypt tokens as part of your key management lifecycle.
- Apply middleware that normalizes error handling for authentication routes to reduce information leakage.
MiddleBrick can help by scanning your Flask API definition and runtime behavior to identify endpoints that process Bearer Tokens and flag inconsistent error handling or lack of authenticated encryption.