Replay Attack in Feathersjs with Basic Auth
Replay Attack in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid authentication request and retransmits it to gain unauthorized access. When Feathersjs is configured with HTTP Basic Auth and lacks protections such as nonce, timestamp, or TLS-only enforcement, this combination becomes susceptible to replay. Basic Auth transmits credentials as a base64-encoded string in the Authorization header; while not plaintext, it is trivial to decode if intercepted. Feathersjs does not inherently add per-request random values or one-time tokens to Basic Auth challenges, so the same Authorization header can be reused across requests.
In a typical Feathersjs Basic Auth setup, the server validates credentials on each call but does not track whether a given nonce or client-generated token has been seen before. An attacker who observes a legitimate Authorization header over a compromised network or via server logs can replay it to access protected endpoints, especially for idempotent operations like GET or queries that do not require additional anti-replay mechanisms. Without additional safeguards, the protocol’s simplicity turns into a weakness: the static or infrequently rotated credentials combined with predictable request patterns enable replay without needing to crack the password.
Operational logging or error messages can further expose timing or request identity, aiding an attacker in correlating intercepted payloads. If TLS is not enforced end-to-end, the base64 string can be lifted from unencrypted segments of traffic. Even with TLS, internal proxies or load balancers that terminate encryption may log headers, creating a replay surface. Feathersjs applications that rely solely on Basic Auth therefore need explicit countermeasures—such as server-side nonce tracking, timestamp windows, or binding requests to a session identifier—to prevent replay within the framework’s default behavior.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
To mitigate replay in Feathersjs with Basic Auth, introduce per-request nonces and timestamps, enforce HTTPS, and avoid storing static credentials in code. Below are concrete, working examples that integrate safely with Feathersjs authentication flow.
Example 1: Basic Auth with nonce and timestamp validation
This approach adds custom headers x-request-nonce and x-request-timestamp, validates them on each request, and rejects replays or stale requests.
// src/authentication.js
const crypto = require('crypto');
module.exports = function (options = {}) {
const NONCE_STORE = new Set();
const TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes
return async context => {
const { headers } = context.params;
const nonce = headers['x-request-nonce'];
const timestamp = headers['x-request-timestamp'];
if (!nonce || !timestamp) {
throw new Error('Missing nonce or timestamp header');
}
const now = Date.now();
if (Math.abs(now - Number(timestamp)) > TIMESTAMP_TOLERANCE_MS) {
throw new Error('Request timestamp out of tolerance');
}
if (NONCE_STORE.has(nonce)) {
throw new Error('Replay detected: nonce already used');
}
NONCE_STORE.add(nonce);
// Optional: prune old nonces periodically
if (NONCE_STORE.size > 10000) {
const entries = Array.from(NONCE_STORE);
NONCE_STORE.clear();
entries.slice(1000).forEach(n => NONCE_STORE.add(n));
}
// Proceed with Basic Auth validation using feathers-authentication or custom hook
return context;
};
};
Apply this hook before your authentication service hook:
// src/hooks/index.js
const nonceValidator = require('./nonce-validator');
app.hooks({
before: {
all: [nonceValidator()],
auth: {
before: {
create: [ /* your existing auth hooks */ ]
}
}
}
});
Example 2: Enforce HTTPS and secure Basic Auth credentials
Ensure TLS is mandatory and credentials are verified against a secure store. This example uses a custom hook to validate credentials while rejecting non-HTTPS requests in production.
// src/hooks/require-https.js
module.exports = function (options = {}) {
return async context => {
if (process.env.NODE_ENV === 'production' && !context.params.secure) {
throw new Error('HTTPS required');
}
return context;
};
};
Combine with a safe credentials check rather than inline secrets:
// src/authentication.js (extended)
const bcrypt = require('bcrypt');
const USERS = new Map([
// In practice, load from a secure DB with proper secret management
// username: { passwordHash, scope }
]);
module.exports = function (options = {}) {
return async context => {
const { username, password } = context.params.payload || {};
if (!username || !password) {
throw new Error('Missing credentials');
}
const userRecord = USERS.get(username);
if (!userRecord) {
throw new Error('Invalid credentials');
}
const match = await bcrypt.compare(password, userRecord.passwordHash);
if (!match) {
throw new Error('Invalid credentials');
}
context.result = { username, scope: userRecord.scope };
return context;
};
};
Register hooks in your Feathers service configuration:
// src/app.js
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const app = express(feathers());
app.configure(express.rest());
app.configure(express.json());
app.configure(express.urlencoded({ extended: true }));
app.use('/', {
async create(data, params) {
// Example protected operation
return { status: 'ok' };
}
});
app.hooks({
before: {
all: [require('./hooks/require-https')()],
auth: {
before: {
create: [require('./hooks/validate-basic-auth')()]
}
}
}
});
These measures ensure that each request is unique and bound to a time window, effectively neutralizing replay while preserving Basic Auth compatibility with Feathersjs.