Side Channel Attack in Feathersjs
How Side Channel Attack Manifests in Feathersjs
Side channel attacks in Feathersjs exploit timing differences and error responses to infer sensitive information about your application's data and structure. The most common manifestation occurs through response timing variations during authentication and data retrieval operations.
Consider a Feathersjs authentication endpoint that checks user credentials. When an attacker submits a username, the response time differs based on whether the username exists in the database. If the username exists, the system performs password verification (bcrypt comparison), which takes measurable time. If the username doesn't exist, the system returns immediately. This timing difference allows attackers to enumerate valid usernames.
async function verifyPassword(username, password) {
const user = await users.find({ query: { email: username } }).then(r => r.data[0]);
// Timing leak: exists() vs. non-existent users have different execution paths
if (!user) {
return false; // Immediate return
}
// bcrypt comparison takes 100-500ms, creating measurable timing difference
return await bcrypt.compare(password, user.password);
}Another Feathers-specific side channel occurs through service method differences. When querying for non-existent resources versus existing ones with different permissions, the response structure and HTTP status codes vary:
// Vulnerable: Different responses reveal resource existence
app.service('users').get(id)
.then(user => res.json(user)) // 200 OK - resource exists
.catch(err => {
if (err.name === 'NotFound') {
res.status(404).json({ error: 'Not found' }); // 404 - resource doesn't exist
} else {
res.status(401).json({ error: 'Unauthorized' }); // 401 - exists but unauthorized
}
});Feathersjs's real-time features via Socket.io create additional side channels. The presence or absence of events on channels can reveal whether users exist and their online status:
const userChannel = app.channel((data) => {
// Timing and existence leak: checking if user exists in channel
return app.service('users').get(data.userId)
.then(() => true) // User exists - channel created
.catch(() => false); // User doesn't exist - no channel
});Feathersjs-Specific Detection
Detecting side channel vulnerabilities in Feathersjs requires both manual code review and automated scanning. The middleBrick scanner specifically identifies timing-based and information-disclosure vulnerabilities in Feathers applications.
middleBrick's detection methodology for Feathersjs includes:
- Timing analysis: Measures response time variations across similar endpoints to identify potential timing side channels
- Response structure analysis: Detects inconsistent error responses that reveal system state
- Authentication flow analysis: Examines login and verification endpoints for timing leaks
- Real-time channel analysis: Scans Socket.io channels for existence-revealing patterns
- Service method analysis: Reviews get(), find(), and other service methods for information disclosure
Using middleBrick's CLI for Feathersjs scanning:
npx middlebrick scan https://your-feathersjs-app.com/api
# Or scan specific Feathers endpoints
npx middlebrick scan https://your-feathersjs-app.com/api/users
npx middlebrick scan https://your-feathersjs-app.com/socket.io/The scanner analyzes your Feathersjs application's runtime behavior, looking for patterns like:
// middleBrick detects this pattern:
if (await userExists(email)) {
// Different code path than non-existent users
return await verifyPassword(email, password);
} else {
return false; // Immediate return creates timing difference
}Manual detection techniques for Feathersjs developers:
// Use performance hooks to identify timing variations
app.service('users').hooks({
before: {
get: async (context) => {
const start = process.hrtime.bigint();
// Your logic here
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1e6;
// Log and analyze timing differences
if (duration > 100) console.log('Potential timing issue detected');
}
}
});Feathersjs-Specific Remediation
Remediating side channel vulnerabilities in Feathersjs requires implementing consistent response patterns and timing across all code paths. Here are Feathers-specific solutions:
1. Constant-time authentication:
// Secure: Constant-time comparison regardless of user existence
async function verifyPassword(username, password) {
// Always perform a database lookup
const user = await users.find({ query: { email: username } }).then(r => r.data[0]) || {
// Return a dummy user with hashed dummy password
password: process.env.DUMMY_HASHED_PASSWORD
};
// Always perform bcrypt comparison
const valid = await bcrypt.compare(password, user.password);
// Always perform same operations regardless of outcome
await logAuthenticationAttempt(username, valid);
return valid;
}2. Consistent error responses:
// Secure: Uniform response structure
app.service('users').hooks({
get: async (context) => {
try {
const result = await context.app.service('users').get(context.id);
context.result = result;
} catch (error) {
// Always return same structure, different status only
context.result = { error: 'Resource not accessible' };
context.statusCode = error.name === 'NotFound' ? 404 : 401;
}
}
});3. Rate limiting to mitigate timing attacks:
const rateLimit = require('express-rate-limit');
// Apply rate limiting to authentication endpoints
app.use('/api/authentication', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: 'Too many authentication attempts, please try again later.',
standardHeaders: true,
legacyHeaders: false,
}));
// Add to Feathers service
app.service('authentication').hooks({
before: {
create: async (context) => {
// Add artificial delay to normalize timing
await new Promise(resolve => setTimeout(resolve, 200));
}
}
});4. Secure real-time channels:
// Secure: Don't reveal user existence through channel presence
const userChannel = app.channel((data) => {
// Always return a channel, don't reveal existence
const channel = app.channel("authenticated");
// Filter events server-side instead of channel presence
return channel.filter((message) => {
return message.userId === data.userId;
});
});