Zone Transfer in Feathersjs
How Zone Transfer Manifests in Feathersjs
Zone Transfer in Feathersjs occurs when authentication boundaries are improperly enforced between service methods, allowing users to access data from service calls they shouldn't be able to make. This manifests in several Feathers-specific patterns:
Service Method Chaining Attacks
class MessageService {
async get(id, params) {
// Authenticated user requests their own message
const message = await this._getFromDb(id);
// Zone transfer vulnerability: calls update on behalf of user
// without proper authorization check
await this.update(id, {
$inc: { viewCount: 1 }
}, params);
return message;
}
async update(id, data, params) {
// Missing authorization: assumes caller is already authenticated
return this._updateInDb(id, data);
}
}
In this Feathersjs pattern, a user can trigger data modifications through service chaining that bypass their actual permissions. The get method calls update internally, creating a zone transfer where the user's initial authenticated context is extended into an unauthorized operation.
Hook Chain Exploitation
const hooks = {
before: {
get: [authenticate('jwt')],
update: [authenticate('jwt')]
},
after: {
get: [async context => {
// Zone transfer via hook chain
// User authorized for GET but not for internal UPDATE
await context.app.service('messages').update(
context.id,
{ $inc: { accessCount: 1 } },
context.params
);
}]
}
}
Feathersjs hooks create a particularly dangerous zone transfer scenario. A user authenticated for a read operation can trigger write operations through hook chains, effectively transferring their authenticated zone into unauthorized territory.
Service Composition Without Authorization Boundaries
class UserService {
async find(params) {
// User searches for their profile
const user = await this._findUser(params.query);
// Zone transfer: composes data from multiple services
// without checking if user can access all components
const recentActivity = await
this.app.service('activity').find({
query: { userId: user.id, limit: 10 }
});
return { user, recentActivity };
}
}
This Feathersjs composition pattern allows users to access data from services they never authenticated against, creating a zone transfer across service boundaries within the same application.
Feathersjs-Specific Detection
Detecting Zone Transfer in Feathersjs requires examining service method calls, hook chains, and service composition patterns. Here's how to identify these vulnerabilities:
Static Code Analysis for Zone Transfer Patterns
// Scan for dangerous service method chaining
function detectZoneTransfer(serviceCode) {
const patterns = [
/get\s*\(\s*[^)]*\)\s*=>\s*\{[^}]*update[^}]*\}/g,
/find\s*\(\s*[^)]*\)\s*=>\s*\{[^}]*update[^}]*\}/g,
/create\s*\(\s*[^)]*\)\s*=>\s*\{[^}]*remove[^}]*\}/g
];
return patterns.some(pattern => pattern.test(serviceCode));
}
// Check hook chains for zone transfer
function analyzeHookChains(hooks) {
if (!hooks.after?.get) return false;
const dangerousOperations = [
'update', 'remove', 'patch', 'create', 'find'
];
return hooks.after.get.some(hook => {
const hookCode = hook.toString();
return dangerousOperations.some(op =>
hookCode.includes(`service('${op}')`) ||
hookCode.includes(`${op}(`)
);
});
}
Runtime Detection with middleBrick
// Using middleBrick CLI to scan Feathersjs API
npx middlebrick scan https://api.yourservice.com/messages
// middleBrick detects zone transfer by:
// - Analyzing service method calls across authentication boundaries
// - Checking hook chains for unauthorized service composition
// - Testing if authenticated users can trigger unauthorized operations
// - Providing a security score with zone transfer findings
Automated Testing for Zone Transfer
const assert = require('assert');
// Test for zone transfer vulnerabilities
async function testZoneTransfer(service) {
// Test if user can trigger unauthorized operations
const userContext = { user: { id: 1, role: 'user' } };
try {
// Attempt to trigger internal service calls
await service.get(1, userContext);
// Check if internal operations were performed
const auditLog = await service.auditLogs.find({
userId: 1,
operation: 'update'
});
assert(auditLog.length === 0, 'Zone transfer detected: unauthorized operation performed');
} catch (error) {
// Expected if proper authorization is in place
}
}
Feathersjs-Specific Remediation
Fixing Zone Transfer in Feathersjs requires implementing proper authorization boundaries between service methods and hook chains. Here are Feathers-specific solutions:
Authorization Guards for Service Methods
const { NotAuthenticated, Forbidden } = require('@feathersjs/errors');
class SecureMessageService {
constructor(options) {
this.app = options.app;
}
// Authorization guard decorator
authorize(operation, requiredRole = null) {
return async (context) => {
const { user } = context.params;
if (!user) {
throw new NotAuthenticated('Authentication required');
}
if (requiredRole && user.role !== requiredRole) {
throw new Forbidden('Insufficient permissions for this operation');
}
// Check if operation is allowed in current context
const allowedOperations = this._getAllowedOperations(user);
if (!allowedOperations.includes(operation)) {
throw new Forbidden(`Operation ${operation} not permitted`);
}
return context;
};
}
async get(id, params) {
// Only allow get if user owns the resource or has admin role
await this.authorize('get', 'admin')(params);
const message = await this._getFromDb(id);
// Safe: no internal service calls that bypass auth
return message;
}
async update(id, data, params) {
// Only allow update if user owns the resource
await this.authorize('update')(params);
return this._updateInDb(id, data);
}
}
Secure Hook Chain Implementation
const hooks = {
before: {
get: [
authenticate('jwt'),
async context => {
const { user, id } = context.params;
// Verify user can access this resource
const resource = await context.app.service('messages').get(id);
if (resource.userId !== user.id && user.role !== 'admin') {
throw new Forbidden('Access denied');
}
}
]
},
after: {
get: [
async context => {
const { user, id } = context.params;
// Safe counter increment: verify permissions first
const messageService = context.app.service('messages');
const message = await messageService.get(id);
if (message.userId === user.id || user.role === 'admin') {
await messageService.patch(id, { $inc: { viewCount: 1 } }, context.params);
}
}
]
}
}
Service Composition with Authorization Boundaries
class SecureUserService {
async find(params) {
const { user } = params;
// Only allow composition if user has permission for all services
const canAccessMessages = await this._canAccessService(user, 'messages');
const canAccessActivity = await this._canAccessService(user, 'activity');
if (!canAccessMessages || !canAccessActivity) {
throw new Forbidden('Insufficient permissions for service composition');
}
// Safe composition: all permissions verified
const [userProfile, recentActivity] = await Promise.all([
this._findUser(params.query),
this.app.service('activity').find({
query: { userId: user.id, limit: 10 }
})
]);
return { user: userProfile, recentActivity };
}
async _canAccessService(user, serviceName) {
// Check user permissions for specific service
const permissions = await this.app.service('permissions').find({
query: { userId: user.id, service: serviceName }
});
return permissions.length > 0;
}
}