{"title":"Padding Oracle via AES-CBC Decryption Error Handling","language":"Python","severity":"High","cwe":"CWE-203","source_lines":[10],"flow_lines":[10,11,12,13,14,15],"sink_lines":[15],"vulnerable_code":"from Crypto.Cipher import AES\nfrom flask import Flask, request, jsonify\nimport base64\n\napp = Flask(__name__)\nHSM_KEY = b'16byte_hsm_key!@'\n\n@app.route('/hsm/decrypt_token', methods=['POST'])\ndef hsm_decrypt_session():\n    encrypted_token = base64.b64decode(request.json['token'])\n    cipher = AES.new(HSM_KEY, AES.MODE_CBC, iv=encrypted_token[:16])\n    try:\n        plaintext = cipher.decrypt(encrypted_token[16:])\n        return jsonify({'status': 'valid', 'data': plaintext.decode()}), 200\n    except Exception as e:\n        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":"from Crypto.Cipher import AES\nfrom Crypto.Util.Padding import unpad\nfrom flask import Flask, request, jsonify\nimport base64\nimport hmac\nimport hashlib\n\napp = Flask(__name__)\nHSM_KEY = b'16byte_hsm_key!@'\nHMAC_KEY = b'hmac_auth_key_32bytes!@#$%^&*(]'\n\n@app.route('/hsm/decrypt_token', methods=['POST'])\ndef hsm_decrypt_session():\n    try:\n        encrypted_token = base64.b64decode(request.json['token'])\n\n        if len(encrypted_token) < 64:  # IV (16) + at least one block (16) + HMAC-SHA256 (32)\n            return jsonify({'status': 'error', 'message': 'Invalid token'}), 400\n\n        # Separate HMAC tag (last 32 bytes) from ciphertext\n        ciphertext_with_iv = encrypted_token[:-32]\n        received_mac = encrypted_token[-32:]\n\n        # Verify HMAC first (Encrypt-then-MAC) using constant-time comparison\n        expected_mac = hmac.new(HMAC_KEY, ciphertext_with_iv, hashlib.sha256).digest()\n        if not hmac.compare_digest(received_mac, expected_mac):\n            return jsonify({'status': 'error', 'message': 'Invalid token'}), 400\n\n        # Only decrypt after HMAC verification passes\n        iv = ciphertext_with_iv[:16]\n        ciphertext = ciphertext_with_iv[16:]\n        cipher = AES.new(HSM_KEY, AES.MODE_CBC, iv=iv)\n        plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)\n\n        return jsonify({'status': 'valid', 'data': plaintext.decode()}), 200\n    except Exception:\n        # Return a generic error message regardless of failure reason\n        return jsonify({'status': 'error', 'message': 'Invalid token'}), 400"}