Data Exposure in Feathersjs with Jwt Tokens
Data Exposure in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time APIs that commonly uses JWT tokens for stateless authentication. When JWT tokens are mishandled in a FeathersJS service, sensitive data can be exposed through multiple vectors. A typical Feathers service exposes endpoints that rely on an authenticated JWT in the Authorization header. If the service returns full user records—including password hashes, secret fields, or internal relations—after validating the token, an authenticated context can still leak sensitive information.
One common pattern is attaching the entire user payload returned by the JWT payload (decoded from the token) directly into the response. For example, a Feathers hook might pass req.user through to the result without filtering. Because JWTs are self-contained, the token may include claims like roles, permissions, or identifiers that should not be surfaced to the client if they are not needed. If the API response includes these fields, an attacker who obtains the token can infer sensitive role mappings or scope, leading to privilege escalation or data exposure beyond what the application intended to reveal.
Another vector involves misconfigured service hooks that merge query parameters or request bodies with the authenticated user object. Suppose a user profile endpoint allows partial updates and directly assigns incoming JSON onto the user record retrieved via the JWT subject. Without strict schema validation, an attacker can supply fields that overwrite sensitive attributes or read fields they should not see. Because FeathersJS often relies on hooks for authorization, a missing or incorrectly ordered hook can skip checks and return raw data that includes fields like internal IDs, email addresses, or tokens intended for backend use only.
Middleware and hook ordering also contribute to exposure. If authentication hooks that decode the JWT are placed after data retrieval hooks, the service may fetch records before confirming the token is valid and that the requesting user is authorized for those records. This ordering flaw can inadvertently expose records belonging to other users when combined with weak or missing ownership checks. Even with valid JWT tokens, an attacker can iterate over identifiers if the service does not enforce row-level ownership based on the JWT subject claim.
Logging and error messages further amplify data exposure. FeathersJS services that log full request or response payloads may inadvertently write JWT claims or user data to logs. If error responses include stack traces or internal field names, they can reveal whether a JWT-based user exists or expose internal data structures. Properly scoped tokens with minimal claims reduce the impact, but developers must ensure that responses do not echo back sensitive fields present in the token or the database record.
Finally, token storage and transmission practices affect exposure. If frontend clients store JWTs in insecure locations and transmit them over non-TLS channels, tokens can be intercepted, leading to unauthorized data access. FeathersJS APIs that do not enforce HTTPS or that accept tokens from insecure origins increase the risk that intercepted tokens will be used to read or modify data. Combining secure transport, conservative token claims, strict hook ordering, and output filtering is essential to prevent data exposure when using JWTs with FeathersJS.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on minimizing claims in the JWT, strict hook ordering, and explicit filtering of returned data. Ensure that tokens issued by your identity provider contain only necessary claims such as sub, role, and scope. In FeathersJS, configure authentication to validate the token and attach a sanitized user object to req.user, excluding sensitive fields before they reach services.
// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');
const jwt = require('jsonwebtoken');
module.exports = function authentication(options) {
return async context => {
const { headers } = context.params;
const token = headers && headers.authorization && headers.authorization.split(' ')[1];
if (!token) {
return context;
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// Sanitize: only keep safe claims
context.params.user = {
id: payload.sub,
role: payload.role,
scope: payload.scope || 'read'
};
// Explicitly remove sensitive claims
delete context.params.user.passwordHash;
delete context.params.user.emailVerifiedAt;
return context;
} catch (error) {
throw new AuthenticationError('Invalid token');
}
};
};
Next, enforce field-level filtering in service find and get methods to avoid returning sensitive fields even if they exist in the database record. Use a hook that removes properties based on the requesting user’s role derived from the JWT claims.
// src/hooks/secure-results.js
module.exports = function secureResults(options) {
return async context => {
const user = context.params.user;
if (!user || !user.role) {
throw new Error('Unauthenticated context');
}
const isAdmin = user.role === 'admin';
// Filter response shape
if (context.result && context.result.data) {
context.result.data = context.result.data.map(entry => sanitizeEntry(entry, isAdmin));
} else if (context.result && context.result.total) {
context.result.data = context.result.data.map(entry => sanitizeEntry(entry, isAdmin));
} else if (context.result) {
context.result = sanitizeEntry(context.result, isAdmin);
}
return context;
};
};
function sanitizeEntry(entry, isAdmin) {
const sanitized = { ...entry };
// Always remove these fields
delete sanitized.passwordHash;
delete sanitized.resetToken;
delete sanitized.emailVerifiedAt;
// Role-based exposure
if (!isAdmin) {
delete sanitized.internalNotes;
delete sanitized.auditLog;
// Ensure PII is limited
if (sanitized.email) {
sanitized.email = sanitized.email.split('@')[0] + '@[hidden]';
}
}
return sanitized;
}
Configure FeathersJS services to apply these hooks in the correct order: authentication first, then authorization, then data retrieval, and finally result filtering. This ordering ensures that the user derived from the JWT is available before any data is fetched and that sensitive fields are removed before the response leaves the server.
// src/services/users/users.service.js
const { iff, isProvider } = require('feathers-hooks-common');
const authenticateJwt = require('./hooks/authentication');
const secureResults = require('./hooks/secure-results');
module.exports = function (app) {
const options = {
name: 'users',
paginate: { default: 25, max: 50 }
};
app.use('/users', createService(options));
const service = app.service('users');
service.hooks({
before: {
all: [iff(isProvider('external'), authenticateJwt)],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
after: {
all: [secureResults()],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
});
};
Additionally, validate and limit the claims accepted from the JWT to prevent token smuggling or privilege escalation via manipulated tokens. Reject tokens with unexpected issuers or audiences, and enforce short expiration times to reduce the window for exposure. Combine these practices with HTTPS enforcement and secure storage on the client to minimize data exposure when JWTs are used with FeathersJS.
Finally, audit your service responses to ensure no sensitive fields leak. Use automated tests that simulate authenticated requests with varying role claims derived from the JWT and assert that restricted fields are absent. This verification complements the runtime hooks and helps maintain a minimal data exposure posture across your FeathersJS API surface.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |