Padding Oracle with Bearer Tokens
How Padding Oracle Manifests in Bearer Tokens
A padding oracle attack exploits the way a system reacts to invalid padding during decryption of ciphertext that uses a block cipher in CBC mode (e.g., AES‑128‑CBC). When a Bearer Token is an encrypted JWT (JWE) that relies on AES‑CBC without an authenticated encryption mode, an attacker can submit modified token fragments to an endpoint and observe whether the server returns a padding‑error versus a generic validation error. The difference in responses (status code, error message, or timing) leaks information about the plaintext, allowing gradual decryption of the token.
Typical vulnerable code path in a Node.js service that accepts a Bearer Token:
const crypto = require('crypto');
function decryptToken(encryptedToken, iv) {
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
let decrypted = decipher.update(Buffer.from(encryptedToken, 'base64'));
decrypted += decipher.final(); // throws if padding is invalid
return JSON.parse(decrypted.toString('utf8'));
}
// Express middleware
app.use((req, res, next) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) return res.status(401).send('Missing token');
const token = auth.slice(7);
const parts = token.split('.'); // JWE format: header.encrypted_key.iv.ciphertext.tag
try {
const payload = decryptToken(parts[3], parts[2]); // ciphertext, iv
req.user = payload;
next();
} catch (err) {
// Different messages for padding vs other failures
if (err.message.includes('bad decrypt')) {
return res.status(400).send('Invalid padding'); // oracle!
}
return res.status(401).send('Invalid token');
}
});
If the server distinguishes "Invalid padding" from "Invalid token", an attacker can flip bits in the ciphertext, send the altered token, and based on the response learn whether the padding was valid. Repeating this across bytes reveals the plaintext without knowing the key — classic padding oracle (see CVE‑2016‑0701 in OpenSSL, CVE‑2016‑2107 in AES‑NI).
In the context of Bearer Tokens, the attack targets any endpoint that:
- Accepts a token via the Authorization: Bearer header.
- Attempts to decrypt the token using AES‑CBC (or another CBC mode) without verifying an authentication tag.
- Returns distinct error responses for padding failures versus other validation errors.
Modern libraries avoid this by using authenticated encryption (AEAD) modes such as AES‑GCM or by employing JWE libraries that enforce constant‑time validation.
Bearer Tokens-Specific Detection
Detecting a padding oracle in a Bearer Token implementation requires observing the server’s reaction to malformed ciphertext. Because the attack is unauthenticated, a black‑box scanner can probe the endpoint without credentials.
middleBrick’s unauthenticated scan includes a specific check for padding oracle vulnerabilities:
- It extracts a valid Bearer Token from a normal response (if any) or generates a syntactically correct JWE with a known plaintext.
- For each byte position of the ciphertext block, it flips a bit and resends the token.
- It records the HTTP status code and response body. A distinct response (e.g., 400 with "Invalid padding") versus a generic 401/400 "Invalid token" indicates an oracle.
- If the pattern is observed across multiple positions, the finding is reported with severity High and remediation guidance.
Example of what middleBrick might report:
| Finding | Severity | Category | Evidence |
|---|---|---|---|
| Padding Oracle in AES‑CBC JWT decryption | High | Cryptographic Failures (OWASP API Top 10 A2:2021) | Flipping bit 3 of the 2nd ciphertext block returned HTTP 400 with body “Invalid padding”; all other bit flips returned HTTP 401 “Invalid token”. |
Because the test only needs to send requests, it works on any publicly accessible API endpoint that accepts Bearer Tokens, matching middleBrick’s 5‑15 second scan window.
Developers can also verify locally by unit‑testing the decryption function: feed it ciphertexts with known‑bad padding and assert that the function throws the same generic error regardless of the padding error type.
Bearer Tokens-Specific Remediation
The correct fix is to eliminate the padding oracle by ensuring that decryption and integrity verification are performed together, in constant time, and that any failure results in the same generic error.
1. Switch to an authenticated encryption mode (AEAD) such as AES‑GCM. This provides confidentiality and integrity in a single step; tampering with the ciphertext will cause authentication failure before any padding check.
const crypto = require('crypto');
function decryptTokenAEAD(encryptedToken, iv, authTag) {
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(key, 'hex'), iv);
decipher.setAuthTag(Buffer.from(authTag, 'base64'));
let decrypted = decipher.update(Buffer.from(encryptedToken, 'base64'));
decrypted += decipher.final(); // throws if tag invalid or ciphertext corrupted
return JSON.parse(decrypted.toString('utf8'));
}
// Usage (JWE with header.encrypted_key.iv.ciphertext.tag)
app.use((req, res, next) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) return res.status(401).send('Missing token');
const token = auth.slice(7);
const [header, encryptedKey, iv, ciphertext, tag] = token.split('.');
try {
const payload = decryptTokenAEAD(ciphertext, iv, tag);
req.user = payload;
next();
} catch (_) {
// Same generic response for any validation failure
return res.status(401).send('Invalid token');
}
});
2. If you must retain a library that only offers CBC mode, always verify an HMAC (or use encrypt‑then‑MAC) before decryption, and ensure the verification constant‑time comparison (e.g., crypto.timingSafeEqual).
const crypto = require('crypto');
function decryptWithMAC(encryptedToken, iv, mac) {
// 1. Verify MAC first (constant time)
const expected = crypto.createHmac('sha256', key)
.update(iv + encryptedToken)
.digest();
if (!crypto.timingSafeEqual(Buffer.from(mac, 'base64'), expected)) {
throw new Error('Authentication failed');
}
// 2. Then decrypt – padding errors now lead to same generic error
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
let dec = decipher.update(Buffer.from(encryptedToken, 'base64'));
dec += decipher.final();
return JSON.parse(dec.toString('utf8'));
}
3. Use a well‑maintained JWE library that handles these details internally, such as @panva/jose for Node.js:
const { jwtDecrypt } = require('panva/jose');
async function handleBearer(req, res, next) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) return res.status(401).send('Missing token');
const token = auth.slice(7);
try {
const { payload, protectedHeader } = await jwtDecrypt(token, key);
req.user = JSON.parse(Buffer.from(payload));
next();
} catch (_) {
return res.status(401).send('Invalid token');
}
};
By applying one of these strategies, the padding oracle is eliminated because the attacker can no longer distinguish padding failures from other validation errors, and any tampering is detected before decryption proceeds.