Rate Limiting Bypass in Feathersjs with Jwt Tokens
Rate Limiting Bypass in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
FeathersJS is a popular framework for building REST and Socket.io APIs. When JWT tokens are used for authentication, developers often rely solely on the token’s validity (signature and expiration) and may apply rate limiting only to unauthenticated endpoints or to a global middleware layer that runs before authentication. This configuration creates a condition where authenticated requests with valid JWT tokens bypass per-user or per-client rate limits.
The bypass occurs because the rate limiter is not inspecting the claims inside the JWT token to enforce identity-based throttling. An attacker who obtains a valid token—through theft, insecure storage, or a compromised client—can make a high volume of requests that all carry the same token. If the rate limiter is scoped to IP addresses only, the attacker can rotate source IPs (e.g., via a botnet or residential proxy pool) while keeping the same token, or if the limiter is applied before authentication, the token is never validated for quota purposes in the rate-limiting logic.
Another common pattern in FeathersJS is using authentication hooks that attach the user object to the request after verifying the JWT, but placing a rate limiter at the service level without considering the authenticated identity. Because service methods receive the context (context.params) containing the user, a naive rate limiter that does not read context.params.user.userId or a claim like sub will treat all requests within the same IP or shared server instance as equivalent, allowing a single token to exhaust the allowed request quota.
Consider an endpoint /messages that allows 60 requests per minute per user. If the limiter is implemented as a global hook that checks only IP and does not inspect the JWT’s sub claim, an attacker with a valid token can send 60 requests rapidly, but subsequent requests across different IPs may still be accepted because the limiter does not associate them with the same user identity encoded in the token.
Real-world attack patterns that exploit this include credential stuffing where a valid token is reused across sessions, or token leakage via logs or browser storage, enabling attackers to construct requests that fall under the same rate-limit bucket. Since FeathersJS often uses hooks for authentication, misordering or incomplete hook composition can compound the issue by failing to enforce identity-aware throttling after authentication.
To detect this using a black-box scan, a tool can attempt to send multiple requests to a protected FeathersJS endpoint using the same JWT token from different source IPs while monitoring whether the server continues to accept requests after the expected quota is exceeded. The presence of per-endpoint or per-IP limits without validation against JWT claims indicates a potential bypass.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation requires aligning rate limiting with the authenticated identity extracted from the JWT. In FeathersJS, this is typically done inside hooks where the authentication hook has already validated the token and attached the user object to context.params. You should implement a rate limiter that uses a key derived from the user identity (e.g., the sub claim or a stable user ID) rather than IP address alone.
Below is an example of a FeathersJS hook that extracts the user ID from a verified JWT and uses a token-bucket style in-memory store for simplicity (in production, use Redis or another shared store for multi-instance deployments). This hook should be placed after the authentication hook so that context.params.user is available.
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = require('./redisClient'); // your Redis client
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 60, // 60 requests
duration: 60 // per 60 seconds
});
const identityRateLimit = (context) => {
if (context.params.user) {
const userId = context.params.user.userId || context.params.user.sub;
const key = `ratelimit_user_${userId}`;
return rateLimiter.consume(key)
.then(() => {})
.catch(() => {
throw new Error('Too many requests');
});
}
return Promise.resolve();
};
module.exports = {
before: {
all: [],
find: [],
get: [],
create: [identityRateLimit],
update: [identityRateLimit],
patch: [identityRateLimit],
remove: [identityRateLimit]
},
after: {},
error: {}
};
This approach ensures that the rate limit key incorporates the user identity from the JWT, preventing bypass via IP rotation when a valid token is used. The key must be namespaced (e.g., ratelimit_user_) to avoid collisions with other rate-limiting schemes.
If you cannot use a shared store, you can implement a simpler in-memory map keyed by user ID, but be aware this does not scale across processes or instances:
const userBucket = new Map();
const inMemoryIdentityRateLimit = (context) => {
if (context.params.user) {
const userId = context.params.user.userId || context.params.user.sub;
const now = Date.now();
const windowMs = 60 * 1000;
const limit = 60;
if (!userBucket.has(userId)) {
userBucket.set(userId, { count: 1, start: now });
return Promise.resolve();
}
const bucket = userBucket.get(userId);
if (now - bucket.start > windowMs) {
bucket.count = 1;
bucket.start = now;
return Promise.resolve();
}
if (bucket.count >= limit) {
return Promise.reject(new Error('Too many requests'));
}
bucket.count += 1;
return Promise.resolve();
}
return Promise.resolve();
};
Additionally, ensure your JWT verification hook is correctly ordered and that tokens with insufficient claims are rejected early. You can also combine IP-based limits with identity-based keys for defense-in-depth, but the identity-based key is the critical factor to prevent bypass when a valid JWT is present.
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |