# Padding Oracle via AES-CBC Decryption Error Handling

Language: Python
Severity: High
CWE: CWE-203

## Source
10

## Flow
10-11-12-13-14-15

## Sink
15

## Vulnerable Code
```python
from Crypto.Cipher import AES
from flask import Flask, request, jsonify
import base64

app = Flask(__name__)
HSM_KEY = b'16byte_hsm_key!@'

@app.route('/hsm/decrypt_token', methods=['POST'])
def hsm_decrypt_session():
    encrypted_token = base64.b64decode(request.json['token'])
    cipher = AES.new(HSM_KEY, AES.MODE_CBC, iv=encrypted_token[:16])
    try:
        plaintext = cipher.decrypt(encrypted_token[16:])
        return jsonify({'status': 'valid', 'data': plaintext.decode()}), 200
    except Exception as e:
        return jsonify({'status': 'invalid_padding', 'error': str(e)}), 400
```

## Explanation

The code implements a padding oracle vulnerability by revealing padding validation status through distinct error responses. When decryption fails due to invalid padding, the error message ('invalid_padding') and 400 HTTP status code leak this information to the caller. An attacker can iteratively modify ciphertext bytes and observe whether the server responds with 200 (valid padding) or 400 (invalid padding) to decrypt arbitrary ciphertext block-by-block without ever knowing the encryption key.

## Remediation

The fix eliminates the padding oracle by implementing Encrypt-then-MAC (HMAC-SHA256) verification before any decryption occurs, using constant-time comparison via hmac.compare_digest to also prevent timing attacks. The minimum token length check is corrected to 64 bytes (16 IV + 16 ciphertext block + 32 HMAC-SHA256). All error conditions now return an identical generic error response with the same HTTP status code regardless of whether the failure was caused by padding, decryption, authentication, or length validation, making it impossible for an attacker to distinguish between error types.

## Secure Code
```python
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from flask import Flask, request, jsonify
import base64
import hmac
import hashlib

app = Flask(__name__)
HSM_KEY = b'16byte_hsm_key!@'
HMAC_KEY = b'hmac_auth_key_32bytes!@#$%^&*(]'

@app.route('/hsm/decrypt_token', methods=['POST'])
def hsm_decrypt_session():
    try:
        encrypted_token = base64.b64decode(request.json['token'])

        if len(encrypted_token) < 64:  # IV (16) + at least one block (16) + HMAC-SHA256 (32)
            return jsonify({'status': 'error', 'message': 'Invalid token'}), 400

        # Separate HMAC tag (last 32 bytes) from ciphertext
        ciphertext_with_iv = encrypted_token[:-32]
        received_mac = encrypted_token[-32:]

        # Verify HMAC first (Encrypt-then-MAC) using constant-time comparison
        expected_mac = hmac.new(HMAC_KEY, ciphertext_with_iv, hashlib.sha256).digest()
        if not hmac.compare_digest(received_mac, expected_mac):
            return jsonify({'status': 'error', 'message': 'Invalid token'}), 400

        # Only decrypt after HMAC verification passes
        iv = ciphertext_with_iv[:16]
        ciphertext = ciphertext_with_iv[16:]
        cipher = AES.new(HSM_KEY, AES.MODE_CBC, iv=iv)
        plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

        return jsonify({'status': 'valid', 'data': plaintext.decode()}), 200
    except Exception:
        # Return a generic error message regardless of failure reason
        return jsonify({'status': 'error', 'message': 'Invalid token'}), 400
```
