Use After Free in Feathersjs with Api Keys
Use After Free in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
A Use After Free condition occurs in a Feathersjs service when a resource (e.g., a data object or hook context) is deallocated or made unavailable while an Api Key–bound request still holds a reference to it, and subsequent operations attempt to use that stale reference. In Feathers, services often retain references to records, hooks, or transport contexts across asynchronous boundaries; if the underlying data structure is reused, mutated, or explicitly released (e.g., via pool return, object invalidation, or framework-level cleanup) while an Api Key validation or authorization step is still in flight, reads or writes can act on freed memory or inconsistent state.
When Api Keys are used for authentication, the key is typically validated early in the request lifecycle—often in a global hook or custom authentication hook—and the associated identity/permissions are attached to the request. If the service implementation or a hook later operates on a shared object that has been freed or reassigned (for example, a cached record that is evicted, a reused event payload, or an improperly scoped variable in an async chain), the request may continue using that invalid memory. This can lead to information disclosure, unexpected mutations, or crashes, depending on what the freed reference points to when accessed.
The combination of Feathersjs’s hook-based architecture and Api Key–driven identity creates specific conditions that can expose Use After Free:
- Shared mutable state across hooks: If a hook mutates or releases an object that is later accessed by downstream hooks or the service method, and an Api Key binding identifies the request but does not guarantee isolation of the object lifecycle, the later access may operate on freed data.
- Asynchronous reuse of records: Feathers services often return promises that resolve to records. If a record is released or overwritten in a cache/pool while the promise chain is still executing under the context of an Api Key–identified request, a Use After Free can occur when the chain tries to read or modify the record.
- Transport or connection objects tied to Api Key sessions: In scenarios where transports (e.g., sockets or REST requests) keep references to request-scoped data for streaming or callbacks, freeing those objects before all callbacks complete while an Api Key is still logically tied to the request can cause use-after-free behavior at the application layer.
Real-world analogs in other ecosystems (such as use-after-free advisories tied to improper memory management in native extensions) map conceptually to Feathers when lifecycle ownership is not carefully isolated between authentication (Api Key validation) and resource handling. Even though Feathers runs in a managed runtime, incorrect handling of object lifetimes in hooks and services can produce security-relevant inconsistencies detectable by middleBrick’s checks for unsafe consumption and property authorization.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring that objects referenced during Api Key–bound authorization remain valid and isolated for the duration of the request. Use immutable copies for records exposed to hooks, enforce strict scoping, and avoid holding references to shared or pooled resources across asynchronous gaps.
Example 1: Isolate records with immutable copies after Api Key validation
After authenticating via an Api Key hook, create a deep copy of any record before passing it through hooks or returning it. This prevents later mutations or frees from affecting the in-flight request.
// src/hooks/authenticate-api-key.js
const crypto = require('node:crypto');
module.exports = function authenticateApiKey(options = {}) {
return async context => {
const { app, method, params } = context;
const providedKey = params.query?.apiKey || context.headers['x-api-key'];
if (!providedKey) {
throw new Error('API key is required');
}
// Validate key against a store (e.g., database, KV)
const keyRecord = await app.service('apiKeys').get(providedKey, { query: { $select: ['id', 'scope', 'hash'] } });
if (!keyRecord || !keyRecord.hash) {
throw new Error('Invalid API key');
}
// Ensure the key is bound to the requested scope
if (!keyRecord.scope || !keyRecord.scope.includes(method)) {
throw new Error('Insufficient scope for API key');
}
// Create an immutable copy to avoid use-after-free across hooks
context.apiKey = Object.freeze({ id: keyRecord.id, scope: [...keyRecord.scope] });
// Optionally redact sensitive fields from params
context.params = { ...context.params, apiKey: undefined };
return context;
};
};
Example 2: Safe service method with scoped data and no cross-request references
Ensure service methods do not reuse objects that may be freed by hooks or external caches. Use local variables and avoid attaching request-bound data to long-lived stores.
// src/services/users/users.class.js
const { Service } = require('feathers');
class UsersService extends Service {
async find(params) {
const { apiKey } = params;
// Local, scoped data; do not hold references to params beyond this call
const users = await super.find({ query: { userId: apiKey?.id } });
// Return a plain, isolated snapshot
return users.map(u => ({ ...u, password: undefined }));
}
async get(id, params) {
const { apiKey } = params;
// Validate scope locally; do not rely on external mutable state
const record = await super.get(id, { query: { userId: apiKey?.id } });
// Return a fresh copy to avoid use-after-free in downstream hooks
return { ...record, token: undefined };
}
}
module.exports = function initAppServices(app) {
const options = { Model: app.get('UserModel'), paginate: { default: 25, max: 50 } };
app.use('/users', new UsersService(options));
};
Example 3: Hook-level isolation to prevent dangling references
In before hooks, avoid mutating shared context objects that could be freed later. Instead, produce new objects for downstream use.
// src/hooks/ensure-record-isolation.js
module.exports = function ensureRecordIsolation(options = {}) {
return async context => {
const { result } = context;
if (result && typeof result === 'object' && !Array.isArray(result)) {
// Replace with a frozen copy to prevent later frees from affecting this request
context.result = Object.freeze({ ...result });
} else if (Array.isArray(result)) {
context.result = result.map(item => Object.freeze({ ...item }));
}
return context;
};
};
Operational guidance
- Use middleware or hooks to validate Api Keys early and freeze or copy sensitive outputs before further processing.
- Avoid caching or pooling record objects that are directly exposed to hooks; prefer immutable snapshots.
- Leverage middleBrick’s checks for unsafe consumption and property authorization to detect patterns that may lead to Use After Free in Feathersjs setups.