Dictionary Attack in Feathersjs with Api Keys
Dictionary Attack in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
A dictionary attack against a Feathersjs service that uses API keys typically targets the endpoint where keys are submitted for authentication. If the API key validation logic performs a direct lookup and then compares keys in a way that leaks timing information, or if the authentication route does not enforce rate limits, an attacker can systematically submit candidate keys and observe differences in response behavior. Feathersjs, being a flexible framework, does not prescribe a single authentication mechanism; developers often add custom authentication hooks or use authentication plugins. When API keys are handled in a service hook without additional protections, each request to that service becomes a potential probe. For example, if a hook retrieves a record by the key value and returns a 401 for missing keys but a 200 with partial data for valid keys, an attacker can infer which keys are valid and refine a dictionary. This pattern is especially risky when the key space is predictable, such as short alphanumeric strings or derived tokens. The absence of per-request rate limiting on the authentication route allows an attacker to send many requests in a short period, increasing the likelihood of a successful guess. Because Feathersjs services often expose rich error messages by default, an attacker can also glean information about the underlying data store or schema from validation failures. In a black-box scan, these timing differences and error behaviors can be detected and correlated to identify valid API keys. The risk is compounded if the same key is used across multiple clients or if keys are not rotated regularly. An attacker leveraging a dictionary list tailored to the expected key format can automate submissions against the Feathersjs endpoint, monitoring responses for incremental changes that indicate correctness. This maps to common OWASP API Top 10 categories such as Broken Object Level Authorization and Improper Rate Limiting, where weak authentication controls enable enumeration and unauthorized access.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
To mitigate dictionary attacks when using API keys in Feathersjs, apply defense-in-depth measures focused on authentication hardening, constant-time comparison, and request throttling. Below are concrete code examples that demonstrate secure handling of API keys.
1. Constant-time key validation
Avoid branching logic that short-circuits on the first mismatched character. Instead, compare the submitted key with the stored key in constant time to prevent timing attacks.
// utils/constantTimeCompare.js
export function constantTimeCompare(a, b) {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
// src/hooks/authenticateApiKey.js
import { constantTimeCompare } from '../utils/constantTimeCompare';
export default function authenticateApiKey(options = {}) {
return async context => {
const { apikey } = context.params.query;
const storedKey = context.app.get('primaryApiKey'); // retrieved securely, e.g., from env
if (!apikey || !storedKey || !constantTimeCompare(apikey, storedKey)) {
context.result = { valid: false };
context.params.authenticated = false;
return context;
}
context.params.authenticated = true;
return context;
};
}
2. Rate limiting on authentication routes
Apply rate limiting at the service hook level to restrict the number of authentication attempts per IP or API key within a sliding window.
// src/hooks/rateLimit.js
export default function rateLimit(options = {}) {
const { windowMs = 60_000, max = 30 } = options;
const requests = new Map();
return async context => {
const ip = context.headers['x-forwarded-for'] || context.connection.remoteAddress;
const now = Date.now();
const key = `rate:${ip}`;
const record = requests.get(key) || { count: 0, reset: now + windowMs };
if (now > record.reset) {
record.count = 0;
record.reset = now + windowMs;
}
record.count += 1;
requests.set(key, record);
if (record.count > max) {
throw new Error('Too many requests');
}
return context;
};
}
// In src/services/keys/keys.hooks.js
import authenticateApiKey from '../hooks/authenticateApiKey';
import rateLimit from '../hooks/rateLimit';
export default {
before: {
all: [authenticateApiKey(), rateLimit({ windowMs: 60_000, max: 30 })],
},
};
3. Secure storage and retrieval of API keys
Store API keys as environment variables or use a secrets manager. Avoid logging keys and ensure that responses do not inadvertently expose them.
// src/hooks/validateKeyAgainstDb.js
export default async function validateKeyAgainstDb(context) {
const { apikey } = context.params.query;
if (!apikey) {
context.params.authenticated = false;
return context;
}
const safeKey = apikey.slice(0, 8); // for logging only, not for comparison
try {
const row = await context.app.service('secrets').Model.findOne({
where: { fingerprint: hashFingerprint(apikey) }
});
if (!row || !constantTimeCompare(apikey, row.encryptedKey)) {
context.params.authenticated = false;
return context;
}
context.result = { clientId: row.clientId };
context.params.authenticated = true;
return context;
} catch (error) {
context.params.authenticated = false;
return context;
}
}
These measures reduce the effectiveness of dictionary attacks by eliminating timing leaks, throttling brute-force attempts, and ensuring keys are handled and stored securely within Feathersjs services.