Api Rate Abuse in Feathersjs with Jwt Tokens
Api Rate Abuse in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for creating JavaScript APIs with real-time capabilities. When endpoints are protected only by JWT tokens and lack explicit rate controls, the API can be subjected to rate abuse. JWTs typically identify and authenticate users without introducing server-side session state. If a FeathersJS service does not enforce per-token or per-subject request limits, an authenticated client can issue many rapid requests that consume server resources, degrade performance, and potentially exhaust connection pools or downstream dependencies.
In a typical FeathersJS setup, authentication might validate a JWT and attach the payload to context.params. Without additional checks, every call to a service—such as creating a record or invoking a custom method—counts toward the same effective quota for that token. Attackers who obtain a valid JWT (through compromise, leakage, or weak issuance policies) can leverage it to hammer rate-sensitive endpoints like login, password reset, or data export. Because JWTs often carry user identifiers (e.g., sub or email), abuse may be targeted at specific accounts or at the global service, depending on how limits are scoped.
The risk is compounded when tokens have long lifetimes or broad scopes. A token issued for read-write access allows the holder to exploit any writeable endpoint at scale. Even when endpoints rely on external rate limiters (e.g., reverse proxy rules), those controls might not differentiate by JWT claims such as roles or tenant IDs. For example, a global limit may protect the service from connection floods but fail to prevent a single user from repeatedly invoking a costly operation that triggers expensive downstream work, leading to denial of experience or unexpected cost spikes if the backend includes third-party API calls.
FeathersJS itself does not provide built-in rate limiting tied to JWT identity. Developers must implement controls at the service method level or via hooks. Without instrumentation that inspects the JWT payload and enforces limits per claim (such as user ID or role), the API remains vulnerable to token replay abuse, where a single token is reused to exceed intended request volumes. This is especially relevant in microservice environments where multiple services share a common authentication layer but each service must independently reason about request budgets.
To detect such issues, middleBrick scans unauthenticated attack surfaces and also evaluates authenticated scenarios when credentials are provided. It examines OpenAPI specs for FeathersJS-style services, cross-references declared security schemes (like JWT bearer tokens) with runtime behavior, and checks whether rate-limiting controls are consistently applied across methods that accept JWT-authenticated requests. Findings include whether rate limiting is missing, whether it is scoped too broadly or too narrowly, and whether token claims are properly considered in enforcement.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on binding rate limits to claims within the JWT and ensuring limits are enforced before expensive operations. Below are concrete FeathersJS examples that incorporate JWT-aware throttling using hooks and an in-memory store for simplicity; in production, use a shared store like Redis.
Example 1: Rate limit by JWT subject in a before hook
const { RateLimiterMemory } = require('rate-limiter-flexible');
// One rate limiter instance can be reused; adjust points and duration as needed
const rateLimiter = new RateLimiterMemory({
points: 60, // 60 requests
duration: 60 // per 60 seconds
});
function rateLimitByJwtSubject() {
return async context => {
const { params } = context;
// FeathersJS sets authenticated user in params.user after authentication hook
if (!params.user || !params.user.sub) {
throw new Error('Unauthorized');
}
const key = `user:${params.user.sub}`;
try {
await rateLimiter.consume(key);
} catch (rej) {
throw new Error('Too many requests');
}
return context;
};
}
// Apply to a specific service
const app = require('@feathersjs/feathers')();
const auth = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
app.configure(auth({
secret: 'your-secret',
entity: 'user',
jwt: {
header: { typ: 'JWT' },
audience: 'https://api.example.com',
issuer: 'feathersjs-app'
}
}));
app.use('/messages', {
async create(data, params) {
// Your business logic
return { result: 'ok' };
}
});
app.service('messages').hooks({
before: {
all: [rateLimitByJwtSubject()], // applies to all methods
create: [rateLimitByJwtSubject()] // explicit for create if needed
}
});
Example 2: Different limits for roles within JWT claims
function roleBasedRateLimiter() {
return async context => {
const { params } = context;
const user = params.user;
if (!user) throw new Error('Unauthorized');
// JWT payload may include roles: ['admin', 'user']
const roles = Array.isArray(user.roles) ? user.roles : ['user'];
let points, duration;
if (roles.includes('admin')) {
points = 300;
duration = 60;
} else {
points = 60;
duration = 60;
}
const key = `user:${user.sub}:${roles.join(',')}`;
const rateLimiter = new RateLimiterMemory({ points, duration });
try {
await rateLimiter.consume(key);
} catch {
throw new Error('Rate limit exceeded for your role');
}
return context;
};
}
app.service('reports').hooks({
before: {
find: [roleBasedRateLimiter()],
get: [roleBasedRateLimiter()]
}
});
Example 3: Global service-level limits with per-method overrides
// Define a map of method-specific limits if needed
const methodLimits = {
create: { points: 30, duration: 60 },
update: { points: 30, duration: 60 },
patch: { points: 30, duration: 60 },
remove: { points: 10, duration: 60 }
};
function configurableRateLimit() {
return async context => {
const { method, params } = context;
const user = params.user;
if (!user) throw new Error('Unauthorized');
const limits = methodLimits[method] || { points: 60, duration: 60 };
const key = `user:${user.sub}:${method}`;
const limiter = new RateLimiterMemory(limits);
try {
await limiter.consume(key);
} catch {
throw new Error('Rate limit exceeded');
}
return context;
};
}
app.service('items').hooks({
before: {
all: [configurableRateLimit()]
}
});
Operational considerations
- Place rate-limiting hooks after authentication so JWT claims are available.
- Use a shared, low-latency store (e.g., Redis) when running multiple instances to synchronize counts across processes.
- Return consistent error codes (e.g., 429 Too Many Requests) and include retry-after headers where appropriate.
- Monitor token usage patterns to adjust points/duration and avoid false positives for legitimate bursts.