Broken Authentication in Feathersjs with Hmac Signatures
Broken Authentication in Feathersjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time web applications that can use HMAC-based authentication to verify the integrity and origin of requests. When HMAC is implemented incorrectly, the authentication boundary breaks, allowing an attacker to impersonate any user or escalate privileges. A common pattern is to sign a subset of the request components (method, path, timestamp, and a user identifier) using a shared secret, then transmit the signature in a header such as x-hmac-signature. If the server recomputes the HMAC using an incomplete or mutable set of data, or if the timestamp window is too permissive, an attacker can replay or manipulate signed requests without knowing the secret.
Consider an endpoint that accepts a JSON payload and uses only the URL and a timestamp for signing. An attacker can intercept a valid signed request and replay it within the allowed time window, because the server does not enforce strict one-time use or nonce tracking. This is a classic broken authentication issue: authentication is present in form (a signature), but it does not guarantee uniqueness or freshness of the transaction. In FeathersJS, if the authentication hook computes the HMAC over the request body but does not bind the signature to a specific resource identifier (such as a record ID or a scoped role), an attacker can substitute the body with another user’s data while keeping the signature valid for a different context.
Another vector arises from inconsistent secret management. If the HMAC secret is embedded in client-side code or shared across multiple services without rotation, compromise of one component exposes the entire system. FeathersJS hooks that verify HMAC must ensure the secret is never derivable from the request and must validate the scope of the signed claims. For example, signing only the user ID without the intended action (e.g., create, update, delete) enables horizontal privilege escalation: a user can reuse their own signed payload to modify other users’ records if the endpoint does not validate ownership at the service layer. The combination of a permissive timestamp window, missing nonce or idempotency key, and insufficient binding of claims to the operation results in broken authentication despite the presence of HMAC.
Real-world attack patterns mirror known weaknesses in signature schemes. Replay attacks occur when a valid signed request is captured and resent to the same endpoint within the allowed time frame. Tampering attacks happen when an attacker modifies non-signature parts of the request, such as swapping the user identifier in the payload, if the server fails to recompute and compare the HMAC over the exact transmitted data. In the context of OWASP API Security Top 10, these map to Broken Object Level Authorization (BOLA) and Broken Authentication. A scanning tool like middleBrick can detect such issues by comparing signed request flows and checking whether the signature scope aligns with the endpoint’s authorization rules, surface findings with severity and remediation guidance tied to compliance frameworks.
Hmac Signatures-Specific Remediation in Feathersjs — concrete code fixes
To remediate broken authentication when using HMAC in FeathersJS, bind the signature to a comprehensive set of request attributes, enforce short and non-reusable timestamps, and validate ownership at the service layer. Below are concrete, syntactically correct examples that demonstrate a robust implementation.
Server-side HMAC verification with strict scope
The server should compute the HMAC over a canonical string that includes the HTTP method, the full request path, a strict timestamp, a nonce, the request body (for POST/PUT), and the user identifier extracted from a prior authenticated step. Use a constant-time comparison to avoid timing attacks.
import crypto from 'crypto';
const HMAC_SECRET = process.env.HMAC_SECRET; // stored securely, e.g., secrets manager
function computeHmac(method, path, timestamp, nonce, body, userId) {
const payload = [
method.toUpperCase(),
path,
timestamp,
nonce,
body ? JSON.stringify(body) : '',
userId
].join('|');
return crypto.createHmac('sha256', HMAC_SECRET).update(payload).digest('hex');
}
function verifyHmac(req, res, next) {
const { timestamp, nonce, 'x-hmac-signature': receivedSignature } = req.headers;
const now = Date.now();
const windowMs = 5 * 1000; // 5 seconds
if (Math.abs(now - Number(timestamp)) > windowMs) {
return res.status(400).send({ message: 'Request expired' });
}
const computed = computeHmac(
req.method,
req.path,
timestamp,
nonce,
req.body,
req.user ? req.user.id : ''
);
const isValid = crypto.timingSafeEqual(
Buffer.from(computed, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
if (!isValid) {
return res.status(401).send({ message: 'Invalid signature' });
}
// Optional: store nonce in a short-lived cache to prevent replays
next();
}
Client-side signing example
The client must include the timestamp, a unique nonce, and the request body when computing the signature. This ensures each request is unique and prevents replay within the timestamp window.
import crypto from 'crypto';
function signRequest(method, path, body, userId, secret) {
const timestamp = Date.now().toString();
const nonce = crypto.randomBytes(16).toString('hex');
const payload = [
method.toUpperCase(),
path,
timestamp,
nonce,
body ? JSON.stringify(body) : '',
userId
].join('|');
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return { timestamp, nonce, signature };
}
// Example usage
const secret = process.env.HMAC_SECRET;
const { timestamp, nonce, signature } = signRequest('POST', '/users', { email: '[email protected]' }, 'usr_123', secret);
fetch('/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-timestamp': timestamp,
'x-nonce': nonce,
'x-hmac-signature': signature
},
body: JSON.stringify({ email: '[email protected]' })
}).then(res => res.json()).then(console.log);
FeathersJS hook integration
Integrate verification as a before hook so that invalid signatures are rejected before service logic runs. Combine this with ownership checks to prevent horizontal privilege escalation.
const { verifyHmac } = require('./hmac-utils');
module.exports = function hmacAuthentication(options = {}) {
return async context => {
// For routes that require HMAC, e.g., data-changing methods
if (['create', 'update', 'patch', 'remove'].includes(context.method)) {
verifyHmac(context.params.raw || context.params, context.response || context.result);
// After verification, enforce ownership if applicable
if (context.params.user) {
const recordUser = await context.app.service(context.path).get(context.id).then(r => r.userId);
if (recordUser !== context.params.user.id) {
throw new Error('Unauthorized: record does not belong to user');
}
}
}
return context;
;
};
By tying the HMAC to the full request context (method, path, timestamp, nonce, body, and user ID), and by validating ownership in the service layer, FeathersJS applications can avoid broken authentication while still benefiting from lightweight HMAC-based integrity checks. These measures align with secure coding practices and reduce the risk of replay, tampering, and privilege escalation.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |