Padding Oracle in Django with Basic Auth
Padding Oracle in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
A padding oracle in Django with HTTP Basic Auth arises when encrypted data (for example, a session token or an API credential blob) is decrypted without integrity verification, and the server reveals whether padding is valid through distinct error responses. In this combination, the attacker controls the ciphertext—often obtained by intercepting the Base64-encoded Authorization header value—and can supply modified ciphertexts to a decryption endpoint or to Django’s session middleware. If the application decrypts the data with a block cipher in CBC mode and returns different HTTP status codes or response bodies for invalid padding versus other decryption failures, the attacker can iteratively decrypt or forge authenticated objects without knowing the secret key.
Consider a scenario where Django stores an encrypted user role in a cookie or a custom header, and the server uses Basic Auth credentials to derive a per-request key material or to select a decryption routine. An attacker who captures the Basic Auth header (e.g., dXNlcjpwYXNz), base64-decodes it to recover the username and password, and then uses that knowledge to manipulate associated encrypted metadata can act as a padding oracle. Because Basic Auth sends credentials with every request, the attacker may also observe timing differences: successful padding leads to further processing (e.g., a 200 response), while invalid padding triggers early rejection with a 400 or 403. These observable differences allow the attacker to decrypt ciphertext block-by-block or to produce a valid ciphertext that grants elevated permissions, effectively bypassing intended access controls.
Django’s default JSON and session handlers do not use authenticated encryption by default, which increases risk when combined with manual cryptographic implementations that misinterpret padding errors. For instance, if a developer writes a custom view that decrypts a token using Crypto.Cipher.AES in CBC mode and explicitly checks padding via a try/except that distinguishes between ValueError from incorrect padding and other exceptions, the server can unintentionally leak padding validity. The presence of Basic Auth does not introduce the padding oracle directly, but it supplies a convenient source of ciphertext and a mechanism for authenticated requests that can be replayed and mutated by the attacker.
Real-world attack patterns aligned with this setup include adaptations of CVE-2016-1000236-like padding oracle behaviors in frameworks, where error messages guide decryption. The OWASP API Security Top 10 category ‘2023-A05: Security Misconfiguration’ and the specific issue of ‘Sensitive Data Exposure’ apply when encrypted data is handled without integrity checks. To mitigate, enforce authenticated encryption with associated data (AEAD) such as AES-GCM, avoid branching on padding validity, and ensure that decryption failures return a uniform error response with the same HTTP status code and timing characteristics.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on removing reliance on Basic Auth for transmitting secrets and on making decryption behavior constant-time and side-channel resistant. Do not derive encryption keys from Basic Auth credentials; instead, use session-based authentication or token-based schemes with proper key management. When encrypted data is required, use AES-GCM or ChaCha20-Poly1305 so that integrity verification is built-in and padding is not meaningful. Below are concrete Django examples that demonstrate secure handling.
Example 1: Using Session Authentication Instead of Basic Auth
Django’s built-in session framework avoids the need to encrypt and decrypt credentials on the client side. This removes the attacker’s ability to supply manipulated ciphertexts derived from Basic Auth headers.
from django.contrib.auth import authenticate, login
from django.http import JsonResponse
from django.views import View
class LoginView(View):
def post(self, request):
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return JsonResponse({'detail': 'Logged in successfully'})
return JsonResponse({'detail': 'Invalid credentials'}, status=401)
Example 2: Secure Encrypted Payload with AES-GCM (No Padding Oracle)
If you must transmit encrypted data, use authenticated encryption. This ensures that tampering is detected before any decryption logic runs, eliminating padding-related side channels.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os, base64, json
def encrypt_data(plaintext: str, key: bytes) -> str:
nonce = os.urandom(12)
aesgcm = AESGCM(key)
data = json.dumps({'payload': plaintext}).encode('utf-8')
ciphertext = aesgcm.encrypt(nonce, data, associated_data=None)
return base64.b64encode(nonce + ciphertext).decode('utf-8')
def decrypt_data(token: str, key: bytes) -> str:
raw = base64.b64decode(token)
nonce, ciphertext = raw[:12], raw[12:]
aesgcm = AESGCM(key)
data = aesgcm.decrypt(nonce, ciphertext, associated_data=None)
return json.loads(data.decode('utf-8'))['payload']
# Usage
key = AESGCM.generate_key(bit_length=256)
token = encrypt_data('sensitive_value', key)
print(decrypt_data(token, key))
Example 3: Uniform Error Handling for Decryption
Ensure that decryption failures, whether due to invalid padding, authentication tag mismatch, or other reasons, return the same HTTP response to prevent information leakage.
from django.http import JsonResponse
from cryptography.exceptions import InvalidTag
def safe_decrypt_view(request):
token = request.headers.get('X-Encrypted-Payload')
if not token:
return JsonResponse({'detail': 'missing token'}, status=400)
try:
data = decrypt_data(token, key)
return JsonResponse({'data': data})
except (InvalidTag, ValueError, TypeError):
# Always return the same status and generic message
return JsonResponse({'detail': 'invalid request'}, status=400)
Example 4: Middleware to Reject Requests with Non-Standard Authorization Schemes
Prevent accidental use of Basic Auth in production by enforcing token-based authorization via middleware.
from django.http import JsonResponse
class RequireTokenAuthMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth = request.headers.get('Authorization', '')
if auth.lower().startswith('basic '):
return JsonResponse({'detail': 'Basic Auth not allowed'}, status=400)
response = self.get_response(request)
return response