Replay Attack in Feathersjs with Jwt Tokens
Replay Attack in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request and retransmits it to reproduce the original effect. In a FeathersJS application that uses JWT tokens for authentication, the risk arises despite JWTs being cryptographically signed, because the signature only guarantees integrity and origin for the token contents at the moment of issuance. If a request is captured in transit, an attacker can replay the same HTTP method, path, and body, and the server will accept it as legitimate as long as the token is valid and has not yet been explicitly invalidated.
FeathersJS itself does not inherently protect against replays; protection depends on how the application uses JWTs and enforces additional constraints. A typical vulnerable pattern involves a Feathers service that only verifies the JWT presence and claims (e.g., sub, role) without ensuring request uniqueness. For example, an authenticated POST to /transfers with a JWT could be replayed to initiate duplicate transactions. Because JWTs often carry an exp timestamp, replay windows are limited to the token’s lifetime; however, tokens with long expirations or refresh mechanisms increase exposure. Additionally, if the Feathers app does not enforce idempotency keys or one-time-use nonces, the same payment or data modification can be applied multiple times with the same token.
Another specific exposure stems from unauthenticated or weakly protected endpoints in a FeathersJS app. If an endpoint accepts sensitive actions (such as password reset or email change) based solely on a JWT without binding the request to a per-request nonce or timestamp, an attacker who captures the request can replay it to escalate privileges or gain unauthorized actions. Even with HTTPS, passive interception followed by replay over the same secure channel can succeed if the server does not track previously seen requests. The interplay between FeathersJS service hooks that trust the JWT and the absence of replay prevention controls such as one-time tokens or strict timestamp windows creates a concrete path for replay attacks.
Moreover, because FeathersJS commonly exposes REST and WebSocket transports, replay risks can differ by transport. A REST endpoint that issues a JWT-based login and returns an access token may inadvertently enable replay if clients do not include a per-request signature derived from the token combined with a nonce. Similarly, WebSocket events authenticated once via JWT and then carrying business logic (e.g., place order) can be replayed through the same connection if the server does not enforce uniqueness on event identifiers. In short, the combination of JWTs in FeathersJS without additional replay countermeasures allows captured requests to be reused within the token’s validity window, leading to unauthorized duplication of operations.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
To mitigate replay attacks in FeathersJS when using JWT tokens, implement per-request uniqueness and strict validation rather than relying on token validity alone. Below are concrete code examples that demonstrate how to introduce nonce or timestamp checks within FeathersJS service hooks.
Example 1: Hook-based nonce validation for a payments service. The client includes a nonce in the payload, and the server ensures the nonce has not been used within the current token’s validity window. This prevents exact request replays.
// src/hooks/replay-protection.js
module.exports = function () {
const seenNonces = new Map(); // In production, use a fast TTL cache or external store
return async context => {
const { params } = context;
const authPayload = params.auth || {};
const token = authPayload.accessToken || authPayload.jwt;
// If JWT is present, decode without verification to inspect exp (or rely on verifiedJwt from auth)
let exp = null;
if (token) {
try {
// In real usage, use a JWT library to verify and decode
const parts = token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
exp = payload.exp;
}
} catch (err) {
throw new Error('Invalid token');
}
}
const nonce = context.data?.nonce;
if (!nonce) {
throw new Error('Missing nonce');
}
const now = Math.floor(Date.now() / 1000);
if (exp && now > exp) {
throw new Error('Token expired');
}
// Simple in-memory nonce tracking; replace with Redis or similar for distributed setups
if (seenNonces.has(nonce)) {
const lastSeen = seenNonces.get(nonce);
// Reject if the same nonce was used within the token's lifetime window
if (now - lastSeen < 60) { // e.g., 60-second window
throw new Error('Duplicate request: possible replay attack');
}
}
seenNonces.set(nonce, now);
// Optional: prune old nonces periodically (not shown)
return context;
};
};
// In a service hook registration
const paymentService = app.service('payments');
paymentService.hooks({
before: {
create: [require('./hooks/replay-protection')()]
}
});
// Example client request including a nonce
// fetch('/api/payments', {
// method: 'POST',
// headers: { Authorization: 'Bearer ' },
// body: JSON.stringify({ amount: 100, nonce: 'unique-request-id-123' })
// });
Example 2: Timestamp-based idempotency key in a transfer service. The client includes both a nonce and a timestamp; the server checks that the timestamp is within an acceptable window and that the nonce has not been used for that timestamp range.
// src/hooks/idempotency-hook.js
const IDEMPOTENCY_TTL = 300; // 5 minutes
const nonceStore = new Map();
module.exports = function () {
return async context => {
const { idempotencyKey, timestamp, ...data } = context.data || {};
if (!idempotencyKey || !timestamp) {
throw new Error('Missing idempotencyKey or timestamp');
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 60) { // allow 60-second clock skew
throw new Error('Timestamp out of acceptable window');
}
const key = `${idempotencyKey}:${timestamp}`;
if (nonceStore.has(key)) {
throw new Error('Duplicate idempotency key within time window');
}
// Store with TTL; production should use a persistent cache
nonceStore.set(key, true);
setTimeout(() => nonceStore.delete(key), IDEMPOTENCY_TTL * 1000);
// Attach normalized data for downstream handlers
context.data = { ...data, idempotencyKey, timestamp };
return context;
};
};
// Usage in a transfers service hook
const transfers = app.service('transfers');
transfers.hooks({
before: {
patch: [require('./hooks/idempotency-hook')()]
}
});
In addition to hook-level protections, ensure that FeathersJS authentication integrates JWT validation with short expirations and that refresh token rotation is employed to limit the lifetime of individual JWTs. Combine these technical controls with operational practices such as monitoring for repeated nonce collisions and logging suspicious replay patterns. These measures reduce the feasibility of replay attacks against FeathersJS endpoints protected by JWT tokens without altering the core authentication behavior.