Side Channel Attack with Basic Auth
How Side Channel Attack Manifests in Basic Auth
Side channel attacks against Basic Auth exploit timing differences and error message variations to extract sensitive information. When an attacker can observe authentication responses, they can distinguish between "invalid username" and "invalid password" errors, allowing username enumeration. This timing attack works because password verification typically takes longer than username lookup.
// Vulnerable Basic Auth timing attack example
const express = require('express');
const app = express();
app.use(express.basicAuth((username, password) => {
// Vulnerable: different timing for username vs password failures
const user = users.find(u => u.username === username);
if (!user) {
return false; // Fast response: username doesn't exist
}
// Slow response: username exists, verifying password
return user.password === password;
}));
The fundamental issue is that Basic Auth implementations often provide different response characteristics based on authentication failure type. An attacker can measure response times to determine if a username exists in the system. This becomes more dangerous when combined with error messages that explicitly state whether the username or password was incorrect.
// Even more vulnerable: explicit error messages
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Different error messages reveal account existence
if (!users[username]) {
return res.status(401).json({
error: 'Invalid username'
});
}
if (users[username].password !== password) {
return res.status(401).json({
error: 'Invalid password'
});
}
res.json({ token: generateToken(username) });
});
Another Basic Auth-specific side channel occurs through HTTP status code variations. Some implementations return 403 Forbidden for invalid credentials while others return 401 Unauthorized. Attackers can use these variations to map out authentication logic and identify protected resources.
// Status code variation side channel
app.get('/protected', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Authentication required'
});
}
const [scheme, credentials] = authHeader.split(' ');
if (scheme.toLowerCase() !== 'basic') {
return res.status(400).json({
error: 'Invalid authentication scheme'
});
}
// Different failure paths create timing variations
const decoded = Buffer.from(credentials, 'base64').toString();
const [username, password] = decoded.split(':');
if (!username || !password) {
return res.status(401).json({
error: 'Missing credentials'
});
}
// Vulnerable timing: database lookup vs. immediate failure
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({
error: 'Authentication failed'
});
}
if (user.password !== password) {
return res.status(401).json({
error: 'Authentication failed'
});
}
res.json({ message: 'Access granted' });
});
Basic Auth-Specific Detection
Detecting side channel vulnerabilities in Basic Auth requires systematic testing of timing variations and error message consistency. The key indicators are:
- Response time differences between valid/invalid usernames
- Distinct error messages for username vs password failures
- HTTP status code variations based on authentication state
- Different processing paths for authentication failures
middleBrick's Basic Auth scanning specifically tests these timing characteristics by making multiple authentication attempts with varying inputs and measuring response characteristics. The scanner uses statistical analysis to identify significant timing differences that indicate information leakage.
// middleBrick CLI scan for Basic Auth side channels
npm install -g middlebrick
# Scan a Basic Auth endpoint
middlebrick scan https://api.example.com/protected \
--auth-type basic \
--username admin \
--password wrong \
--verbose
# Output includes timing analysis and error message consistency
# Example findings:
# - Timing variance: 45ms (high risk)
# - Error message leakage: username enumeration possible
# - Status code inconsistency: 401 vs 403 variations detected
For manual testing, you can use timing analysis tools to measure authentication response variations. The following script demonstrates how to test for username enumeration vulnerabilities:
const axios = require('axios');
const crypto = require('crypto');
async function testTiming(username) {
const start = Date.now();
try {
const response = await axios.get('https://api.example.com/protected', {
auth: { username, password: 'invalid' },
validateStatus: () => true
});
return {
status: response.status,
time: Date.now() - start,
message: response.data?.error || 'No message'
};
} catch (error) {
return {
status: error.response?.status || 500,
time: Date.now() - start,
message: error.response?.data?.error || 'No message'
};
}
}
// Test multiple usernames to detect timing patterns
async function analyzeBasicAuth() {
const testUsers = ['admin', 'user', 'test', 'nonexistent'];
const results = await Promise.all(testUsers.map(testTiming));
console.log('Timing Analysis:');
results.forEach((result, i) => {
console.log(`${testUsers[i]}: ${result.time}ms, Status: ${result.status}, Message: ${result.message}`);
});
// Analyze for timing differences
const times = results.map(r => r.time);
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const variance = Math.max(...times) - Math.min(...times);
console.log(`Average: ${avgTime}ms, Variance: ${variance}ms`);
if (variance > 30) {
console.log('⚠️ High timing variance detected - potential username enumeration');
}
}
analyzeBasicAuth();
middleBrick's automated scanning goes further by testing across multiple dimensions simultaneously and providing severity scores based on the combination of timing, error message, and status code analysis.
Basic Auth-Specific Remediation
Fixing side channel vulnerabilities in Basic Auth requires eliminating timing differences and standardizing error responses. The most effective approach is to use constant-time comparison functions and uniform error handling.
// Secure Basic Auth implementation
const express = require('express');
const crypto = require('crypto');
const app = express();
// Constant-time string comparison to prevent timing attacks
function constantTimeCompare(val1, val2) {
if (val1.length !== val2.length) return false;
let result = 0;
for (let i = 0; i < val1.length; i++) {
result |= val1.charCodeAt(i) ^ val2.charCodeAt(i);
}
return result === 0;
}
// Uniform error response to prevent information leakage
function createErrorResponse() {
return {
error: 'Authentication failed',
timestamp: Date.now()
};
}
app.use(express.basicAuth((username, password) => {
// Simulate constant-time database lookup
const start = process.hrtime.bigint();
// Always perform full lookup to prevent timing differences
const user = users.find(u => u.username === username) || { password: null };
// Always perform password comparison, even if user doesn't exist
const validPassword = constantTimeCompare(password, user.password || '');
// Add constant delay to normalize response times
const elapsed = Number(process.hrtime.bigint() - start) / 1000000;
const delay = Math.max(0, 50 - elapsed); // 50ms minimum response time
// Return true only if both username exists and password matches
return validPassword && user.password !== null;
}));
// Middleware to add uniform response headers
app.use((req, res, next) => {
res.setHeader('X-Authentication-Method', 'Basic');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// Standardized error handler
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
console.error('Authentication error:', err.message);
res.status(401).json(createErrorResponse());
});
For applications using Basic Auth with JSON Web Tokens (JWT), the remediation approach includes additional safeguards:
const jwt = require('jsonwebtoken');
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
return res.status(401).json(createErrorResponse());
}
try {
const base64Credentials = authHeader.substring(6);
const credentials = Buffer.from(base64Credentials, 'base64').toString();
const [username, password] = credentials.split(':');
// Constant-time authentication
const user = users.find(u => u.username === username) || { password: null };
const valid = constantTimeCompare(password, user.password || '');
if (!valid || !user.password) {
// Simulate processing time for valid usernames
const start = process.hrtime.bigint();
while (process.hrtime.bigint() - start < 50000000n) {}
return res.status(401).json(createErrorResponse());
}
// Create JWT token
const token = jwt.sign(
{ userId: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token, user: { id: user.id, username: user.username } });
} catch (error) {
res.status(401).json(createErrorResponse());
}
}
The key remediation principles are: use constant-time comparisons, standardize error messages, add uniform delays to normalize response times, and avoid conditional logic that creates different execution paths based on authentication state.