Password Spraying in Feathersjs with Api Keys
Password Spraying in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for creating JavaScript and TypeScript APIs. It supports multiple authentication mechanisms, including token-based strategies and custom hooks. When an API key mechanism is implemented for service-to-service access, authentication bypass or weak rate limiting can allow password spraying to be combined with API key usage.
Password spraying is an attack that attempts a small number of common passwords against many accounts to avoid account lockouts. In a FeathersJS service, if authentication first validates an API key and then falls back to a user/password flow without strict separation, an attacker may enumerate users and spray passwords while using a stolen or leaked API key to bypass IP-based restrictions or gain contextual access. This can happen when the API key is treated as a permissive credential that relaxes rate-limiting or logging for the subsequent password-based step.
Consider a FeathersJS app where an API key is checked in a before hook, and if valid, the hook sets a context user but still allows a password-based auth call later in the chain. If global rate limiting is applied only to unauthenticated requests and the API key grants elevated trust, an attacker can perform extensive password attempts under the guise of an authorized service identity. This exposes user accounts even when the API key itself is not the direct credential for sign-in.
Additionally, if the API key is embedded in client-side code, logs, or error messages, it may be leaked. An attacker who obtains the key can reduce anonymity by correlating requests with user enumeration endpoints, making password spraying more targeted. OWASP API Security Top 10 items such as Broken Object Level Authorization (BOLA) and Excessive Data Exposure can align with this scenario when access controls are not strictly enforced between the API key identity and the password verification path.
Real-world examples include services where an API key is accepted at the transport layer but the application still uses local username/password validation without additional context checks. If the API key does not enforce scope or resource-level permissions, an attacker can leverage it to probe multiple user IDs and spray passwords across accounts, increasing the likelihood of successful compromise without triggering defenses.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on strict separation of API key usage and password-based authentication, enforcing rate limits, and ensuring that API keys do not inadvertently weaken identity checks.
- Do not allow API keys to bypass password rate limiting. Apply rate limits at the service or global level regardless of authentication context.
- Treat API keys as high-privilege credentials. Do not use them to shortcut identity verification when passwords are involved.
- Scope API keys to specific services and enforce granular permissions to reduce lateral exposure.
Code example: FeathersJS authentication with API keys and strict hooks
Use a custom hook to validate API keys and ensure they do not suppress rate limiting or weaken password checks. Below is a server-side hook example that enforces per-identity rate limiting when an API key is used and rejects requests that attempt password-based escalation without explicit consent.
// src/hooks/rate-limit-apikey.js
const { Forbidden } = require('@feathersjs/errors');
module.exports = function rateLimitApiKey(options = {}) {
return async context => {
const { params } = context;
const apiKey = params.headers && params.headers['x-api-key'];
if (!apiKey) {
// No API key, proceed with normal flow
return context;
}
// Validate API key via your service (pseudo implementation)
const apiKeyService = context.app.service('api-keys');
const keyRecord = await apiKeyService.get(apiKey).catch(() => null);
if (!keyRecord) {
throw new Forbidden('Invalid API key');
}
// Enforce rate limiting tied to the key identity to prevent abuse
const rateLimiter = context.app.get('rateLimiter');
const allowed = await rateLimiter.check(keyRecord.userId || keyRecord.scope, options.limit || 100);
if (!allowed) {
throw new Forbidden('Rate limit exceeded for API key');
}
// Attach identity for downstream services, but do not skip password checks
context.params.apiKeyContext = {
scope: keyRecord.scope,
userId: keyRecord.userId
};
return context;
};
};
Below is an example of a FeathersJS service configuration that separates API key authentication from local password strategies and ensures that password-based auth remains subject to its own protections.
// src/services/auth/auth.class.js
const { AuthenticationError } = require('@feathersjs/errors');
const { iff, isProvider } = require('feathers-hooks-common');
module.exports = class AuthService {
constructor(options) {
this.options = options || {};
}
async setup(app) {
this.app = app;
}
async find(params) {
// This service does not support find all
throw new AuthenticationError('Not found');
}
async get(userId, params) {
// Example: ensure password-based auth is not skipped when API key is present
const hasApiKey = params.headers && params.headers['x-api-key'];
if (hasApiKey) {
// API key context is available from a hook; do not auto-login
throw new AuthenticationError('Use explicit auth endpoint for credentials');
}
// Perform user lookup and password verification via an explicit strategy
const users = this.app.service('users');
return users.get(userId, params);
}
async create(data, params) {
// Explicit local login: password + optional second factor
const { email, password } = data;
const user = await this.app.service('users').find({ query: { email } });
if (user.total === 0) {
// Still fail even if API key present to avoid user enumeration leaks
throw new AuthenticationError('Invalid credentials');
}
const userRecord = user.data[0];
const valid = await this.verifyPassword(password, userRecord.passwordHash);
if (!valid) {
throw new AuthenticationError('Invalid credentials');
}
return { accessToken: 'token-here', userId: userRecord.id };
}
async verifyPassword(input, storedHash) {
// Use a robust library, e.g., bcrypt
const bcrypt = require('bcrypt');
return bcrypt.compareSync(input, storedHash);
}
};
Configure hooks to ensure password-based routes remain protected even when an API key is present:
// src/app.js (hooks registration)
const rateLimitApiKey = require('./hooks/rate-limit-apikey');
app.use('/auth', require('./services/auth').create());
app.service('auth').hooks({
before: {
all: [
// Apply API key validation without disabling password checks
rateLimitApiKey({ limit: 50 }),
// Ensure password routes have additional checks
(context) => {
if (context.method === 'create' && context.data.password) {
// Enforce strict rate limiting on password attempts
const limiter = context.app.get('passwordRateLimiter');
const identifier = context.data.email || context.params.ip;
return limiter.consume(identifier).then(() => context).catch(() => {
throw new Error('Too many attempts');
});
}
return context;
}
]
}
});
These examples demonstrate how to keep API keys and password flows independent, enforce per-key and per-user rate limits, and avoid treating API keys as a shortcut for credential validation. This reduces the risk of password spraying when API keys are present.