Padding Oracle in Feathersjs with Jwt Tokens
Padding Oracle in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A padding oracle attack can occur in a FeathersJS application that uses JWT tokens for authentication when the server reveals information about the validity of a padding structure during token verification. This typically arises if the JWT library or the surrounding middleware leaks distinct errors for invalid padding versus invalid signatures, allowing an attacker to iteratively decrypt an encrypted JWT (e.g., using algorithms like AES-CBC) by observing differences in HTTP response behavior or timing. FeathersJS itself is framework-agnostic about transport and storage, so the risk emerges from how JWTs are handled rather than from FeathersJS core features.
For example, if you configure Feathers to use a JWT strategy with an encrypted token format (non-standard signed tokens), and the decryption step is implemented or exposed in a way that returns different error messages or status codes for padding errors versus signature failures, an attacker can use this side channel to gradually recover the plaintext. In a typical FeathersJS service setup, JWT handling often occurs in hooks or custom authentication code; if these routines do not use constant-time verification and instead rely on standard library calls that produce variable errors, the unauthenticated attack surface increases. The attack chain involves intercepting a token, modifying ciphertext blocks, and observing changes in authorization outcomes (e.g., 401 versus 403 or different error payloads), which can reveal the encryption key or allow token forgery.
To illustrate, consider a FeathersJS hook that manually decrypts a JWT using Node.js crypto with AES-CBC. If the hook does not normalize errors and instead propagates padding errors directly, an attacker can exploit this behavior. Below is a vulnerable example that demonstrates the problematic pattern:
const crypto = require('crypto');
const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const app = feathers();
app.configure(authentication({ secret: 'super-secret-key' }));
app.configure(jwt());
// Vulnerable custom hook that decrypts JWT and reveals padding errors
app.hooks({
before: {
all: [context => {
const token = context.headers.authorization?.split(' ')[1];
if (!token) return context;
try {
const algorithm = { cipher: 'aes-256-cbc', key: Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), iv: Buffer.from('1234567890123456') };
const decipher = crypto.createDecipheriv(algorithm.cipher, algorithm.key, algorithm.iv);
let decrypted = decipher.update(token, 'base64', 'utf8');
decrypted += decipher.final('utf8'); // This can throw on bad padding
context.result = { token, decrypted };
} catch (error) {
// Distinguishing error types can leak padding information
if (error.message.includes('padding')) {
throw new Error('Invalid padding');
}
throw new Error('Invalid token');
}
return context;
}]
}
});
In this snippet, the decipher.final('utf8') call throws an error when padding is incorrect, and the error message can be distinguishable to the client. An attacker can use this to mount a padding oracle attack by sending modified tokens and observing responses. Standard JWT libraries (like jsonwebtoken) do not use encrypted JWTs by default; they sign tokens (e.g., HS256 or RS256). Padding oracles are relevant when encryption is used, which is less common for JWTs but can be introduced by custom implementations. FeathersJS does not mandate this pattern, so developers should avoid manual decryption and rely on the framework’s built-in JWT strategies that validate signatures, not decrypt content.
To mitigate, ensure that JWT verification uses standard, high-level APIs that do not expose low-level error distinctions, and that errors are handled uniformly. Avoid implementing custom decryption logic for JWTs unless absolutely necessary, and if encryption is required, use authenticated encryption modes (e.g., AES-GCM) that do not require padding and thus eliminate padding oracle risks. Continuous scanning with tools like middleBrick can help detect such misconfigurations in your API endpoints.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on using FeathersJS’s built-in JWT strategy correctly and ensuring error handling does not leak padding-related information. Prefer signed tokens (e.g., HS256) over custom encrypted JWTs, and if encryption is unavoidable, use authenticated encryption with associated data (AEAD) such as AES-GCM. Always normalize errors to a generic message and use constant-time verification patterns where applicable.
Below are concrete, safe code examples for a FeathersJS service that avoid padding oracle risks:
- Standard JWT authentication with HS256 using FeathersJS built-in strategy:
const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const app = feathers();
app.configure({
secret: 'your-super-secret-key'
});
app.configure(authentication({
entity: 'user',
service: 'users',
jwt: {
secret: 'your-super-secret-key'
}
}));
// A safe hook that does not perform manual decryption
app.use('/messages', {
async create(data, { params }) {
// Authentication and authorization handled by FeathersJS JWT strategy
return { text: data.text };
}
});
- If you must use encryption, prefer an AEAD mode like AES-GCM and ensure errors are generic:
const crypto = require('crypto');
const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const app = feathers();
app.configure(authentication({ secret: 'super-secret-key' }));
app.hooks({
before: {
all: [context => {
const token = context.headers.authorization?.split(' ')[1];
if (!token) return context;
try {
const algorithm = { cipher: 'aes-256-gcm', key: Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex'), iv: Buffer.from('123456789012345612345678', 'hex') };
const decipher = crypto.createDecipheriv(algorithm.cipher, algorithm.key, algorithm.iv);
// In GCM, authentication tag is typically appended or handled separately
const decipherBuffer = Buffer.from(token, 'base64');
const tag = decipherBuffer.slice(-16);
const ciphertext = decipherBuffer.slice(0, -16);
decipher.setAuthTag(tag);
let decrypted = decipher.update(ciphertext);
decrypted = Buffer.concat([decrypted, decipher.final()]);
context.result = { token, decrypted: decrypted.toString('utf8') };
} catch (error) {
// Always return a generic error to avoid leaking padding or authentication details
throw new Error('Invalid token');
}
return context;
}]
}
});
- Centralize error handling in your FeathersJS error handlers to ensure consistent responses:
const { GeneralError } = require('@feathersjs/errors');
app.use((error, req, res, next) => {
// Log the original error for internal debugging
console.error('Auth error:', error);
// Always send a generic message to the client
throw new GeneralError('Unauthorized');
});
These practices align with remediations for common web vulnerabilities such as OWASP API Top 10:2023 broken object level authorization and improper error handling. They ensure that JWT processing in FeathersJS does not expose low-level error distinctions that could facilitate a padding oracle attack.