Zone Transfer in Feathersjs with Jwt Tokens
Zone Transfer in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A Zone Transfer in the context of FeathersJS with JWT tokens occurs when an attacker manipulates or exploits authorization gaps to retrieve sensitive data that should be restricted by record ownership or scope. FeathersJS is a framework that typically relies on services and hooks to enforce data access rules. When JWT tokens are used for authentication and authorization, the vulnerability arises if the service does not properly scope records to the claims encoded in the token, or if the token is accepted without strict validation of scopes and tenant/zone identifiers.
Consider a multi-tenant application where each tenant (or zone) is identified by a claim such as zone_id in the JWT payload. If a FeathersJS service queries the database without filtering by zone_id, or if the query allows an attacker to omit or override the zone filter via crafted parameters, a Zone Transfer can occur — one tenant can access another tenant’s data. This is often an Insecure Direct Object Reference (IDOR) or Broken Object Level Authorization (BOLA) issue that is aggravated by permissive JWT validation.
FeathersJS services commonly define a find or get method that maps to database queries. If the service relies solely on the authenticated user object provided by the JWT strategy without applying additional row-level filters, the attacker may supply an ID that belongs to another zone. For example, an authenticated user with a valid JWT might request /records/123, and if the server retrieves the record by ID only, without verifying that the record’s zone_id matches the zone_id in the token, unauthorized data access occurs.
Attackers may also probe for Zone Transfer weaknesses by enumerating IDs or leveraging mass assignment vulnerabilities to inject or modify zone identifiers. If the FeathersJS hook does not sanitize or enforce immutable zone constraints, tampered requests can bypass intended boundaries. The JWT token itself may be valid, but the application fails to enforce that the token’s zone context is applied consistently across all service operations.
To detect such issues, scanning tools evaluate whether service queries incorporate token claims into data retrieval logic. They check whether parameters like IDs are resolved against the authenticated user’s zone or tenant context, and whether query constraints are enforced before data is returned. Without these checks, an API can unintentionally expose records across zones, violating data isolation principles.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring every FeathersJS service enforces zone scoping using claims from the JWT token. The token payload must be validated and mapped to a query filter so that data access is constrained to the authorized zone.
1. Embed zone context in JWT payload and validate on authentication
When issuing tokens, include a zone_id claim. Validate and normalize this claim during authentication so it is attached to the params object that FeathersJS passes to hooks and services.
// auth.js — local strategy example with zone claim
const { AuthenticationError } = require('@feathersjs/errors');
module.exports = {
async authenticate(entity, { email, password, zone_id }, { app }) {
const userService = app.service('users');
const user = await userService.find({ query: { email } });
if (!user.data || user.data.length === 0) {
throw new AuthenticationError('Invalid credentials');
}
const userRecord = user.data[0];
// Verify password (use a proper library in production)
const valid = await verifyPassword(password, userRecord.passwordHash);
if (!valid) {
throw new AuthenticationError('Invalid credentials');
}
// Ensure the authenticated user belongs to the requested zone
if (userRecord.zone_id !== zone_id) {
throw new AuthenticationError('Unauthorized zone access');
}
return {
accessToken: app.passport.createJWT({
...userRecord,
zone_id: zone_id // include zone in token
}),
token: app.passport.createJWT({
...userRecord,
zone_id: zone_id
}),
user: userRecord
};
}
};
2. Apply zone filter in service hooks
Use a before hook to inject the zone constraint into params.query for all service methods. This ensures that find, get, create, update, and patch respect the zone context from the JWT.
// hooks/zones.js
const { iff, isProvider } = require('feathers-hooks-common');
module.exports = function zoneHook() {
return async context => {
const { user } = context.params;
if (user && user.zone_id) {
// Apply zone filter for queries that support query object
if (context.params.query && typeof context.params.query === 'object') {
// Prevent override by client-supplied zone_id
context.params.query.zone_id = user.zone_id;
}
// For `get` and `remove`, ensure params.id is scoped — handled in service or via additional checks
}
return context;
};
};
Register the hook in your service configuration:
// services/records/records.js
const zoneHook = require('../../hooks/zones');
module.exports = function (app) {
const options = {
name: 'records',
paginate: { default: 10, max: 50 }
};
// Initialize service
const service = app.service('records');
service.hooks({
before: {
all: [zoneHook()],
find: [validateOwnZoneAccess(app)], // optional additional validation
get: [ensureRecordBelongsToZone(app)],
create: [attachZoneFromToken],
update: [strictZoneUpdate],
patch: [strictZonePatch]
}
});
};
3. Enforce zone checks in custom get/remove logic
For get and remove, implement a hook that loads the record and compares its zone with the token’s zone before allowing the operation.
// hooks/zone-validation.js
async function ensureRecordBelongsToZone(app) {
return async context => {
if (context.id != null) {
const recordService = app.service(context.path);
const record = await recordService.get(context.id, context.params);
const { user } = context.params;
if (!record || record.zone_id !== user.zone_id) {
throw new NotFound('Record not found or access denied');
}
// Attach record to context for later use
context.record = record;
}
return context;
};
}
4. Avoid client-controlled zone overrides
Ensure that the zone identifier cannot be set or modified by the client during create or patch. Use a before hook to strip or enforce the zone value.
function attachZoneFromToken() {
return context => {
const { user } = context.params;
if (user && user.zone_id) {
context.data.zone_id = user.zone_id;
}
return context;
};
}
function strictZoneUpdate() {
return context => {
// For updates, ignore any zone_id provided by the client
if (context.data.zone_id) {
delete context.data.zone_id;
}
return context;
};
}
function strictZonePatch() {
return context => {
if (context.data.zone_id) {
delete context.data.zone_id;
}
return context;
};
}
These patterns ensure that JWT tokens are treated as a source of truth for zone scoping and that FeathersJS services consistently enforce zone boundaries, mitigating Zone Transfer risks.