Prototype Pollution in Feathersjs with Jwt Tokens
Prototype Pollution in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Prototype pollution in a FeathersJS application that uses JWT tokens typically arises when untrusted input reaches object merge operations before token validation or after token-based user lookup. FeathersJS is a framework that encourages service-oriented design with hooks and declarative APIs. If a hook or service method merges user-supplied data (e.g., query or body parameters) into a target object such as params.query, params.sequelize, or a user profile retrieved via a JWT payload, an attacker can inject properties like __proto__, constructor.prototype, or specific keys that affect object inheritance across the runtime. This can change default behavior for all objects, enabling escalation techniques such as tampering with role claims inside the JWT or bypassing authorization checks that rely on mutated prototypes.
When JWT tokens are involved, the vulnerability chain often starts with a trusted token that authorizes a request. FeathersJS commonly verifies the token and attaches the decoded payload to params.user. If the application later merges user-controlled data into the decoded payload or into objects derived from it, an attacker can supply properties that modify the prototype chain. For example, an attacker might send a payload like { "email": "alice@example.com", "__proto__": { "isAdmin": true } } in a request body or query, and if FeathersJS merges this into the user object derived from the JWT, the mutated prototype can cause the server to treat the user as an administrator. This is especially dangerous when authorization logic uses inherited properties or when the framework internally relies on object equality or property presence checks.
The risk is compounded if the application uses libraries that perform shallow merges or extend objects without safe guards. For instance, using Object.assign or the spread operator on objects derived from JWT claims can propagate polluted properties into downstream logic. Additionally, if the FeathersJS app exposes management endpoints or configuration routes that accept JSON payloads and later use those values in service methods, an attacker might leverage prototype pollution to manipulate service discovery, logging, or data retrieval behavior. Because JWT tokens often carry role or scope information, successful pollution can lead to privilege escalation, information disclosure, or logic bypass, aligning with common API security risks such as BOLA/IDOR when object identifiers are derived from polluted prototypes.
In the context of the 12 parallel security checks run by middleBrick, prototype pollution would be surfaced under Property Authorization and Input Validation findings. The scanner inspects OpenAPI/Swagger specs and runtime behavior to detect risky merges and missing schema constraints, cross-referencing definitions with runtime observations. This helps identify whether user-supplied data can influence object prototypes in ways that affect JWT-based authorization. Developers should treat JWT tokens as immutable sources of identity and avoid merging external data into decoded payloads or related objects without strict schema validation and property filtering.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on preventing untrusted data from reaching object merge operations and ensuring JWT claims are treated as read-only. Use strict schema validation for all incoming data and avoid mutating objects derived from token payloads. Below are concrete examples for a FeathersJS service that uses JWT authentication.
Example 1: Safe JWT verification and immutable payload usage
Configure the authentication hook to verify JWTs and attach a frozen copy of the payload to params.user. This prevents later hooks or service methods from accidentally modifying the user object.
// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');
const jwt = require('jsonwebtoken');
module.exports = function authHook(options = {}) {
return async context => {
const { authorization } = context.headers;
if (!authorization || !authorization.startsWith('Bearer ')) {
throw new AuthenticationError('Invalid token');
}
const token = authorization.slice(7);
let payload;
try {
payload = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new AuthenticationError('Invalid token');
}
// Freeze the payload to prevent mutation
context.params.user = Object.freeze({ ...payload });
return context;
};
};
Example 2: Validate input and avoid merging into user objects
In a service method, validate incoming data against a strict schema and do not merge it into params.user. Use libraries like Ajv or Zod for validation.
// src/services/users/users.hooks.js
const { iff, isProvider } = require('feathers-hooks-common');
const Ajv = require('ajv');
const ajv = new Ajv();
const userUpdateSchema = {
type: 'object',
required: ['email'],
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string' }
},
additionalProperties: false
};
const validate = ajv.compile(userUpdateSchema);
module.exports = {
before: {
all: [],
find: [],
get: [],
create: [validateInput(userUpdateSchema)],
update: [validateInput(userUpdateSchema)],
patch: [validateInput(userUpdateSchema)],
remove: []
}
};
function validateInput(schema) {
return context => {
const valid = validate(schema, context.data);
if (!valid) {
throw new Error('Invalid input: ' + validate.errors.map(e => e.message).join(', '));
}
// Explicitly avoid merging data into params.user
return context;
};
}
Example 3: Protect against prototype pollution in custom logic
If you must merge data, use a safe merge that filters out dangerous keys such as __proto__, constructor, and prototype. Prefer libraries like lodash's mergeWith with a customizer or use Object.fromEntries with filtering.
// src/utils/safe-merge.js
function safeMerge(target, source) {
const blockedKeys = new Set(['__proto__', 'constructor', 'prototype']);
const entries = [];
for (const key of Object.keys(source)) {
if (!blockedKeys.has(key)) {
entries.push([key, source[key]]);
}
}
return Object.assign(target, Object.fromEntries(entries));
}
// Usage in a hook or service
const merged = safeMerge({}, params.userProvided);
Example 4: Enforce property authorization checks
Even after authentication, ensure that sensitive operations validate claims directly from the verified token rather than from object properties that may have been polluted. Compare roles using the canonical JWT payload fields.
// src/hooks/authorize-admin.js
module.exports = function authorizeAdmin(context) {
const user = context.params.user; // frozen, verified payload
if (!user || user.role !== 'admin') {
throw new Error('Unauthorized');
}
return context;
};
By combining strict validation, immutable payloads, and explicit property checks, you reduce the risk that prototype pollution can affect JWT-based authorization in FeathersJS. These practices align with secure coding guidance and help meet compliance mappings to frameworks such as OWASP API Top 10 and SOC2.