Insecure Design in Feathersjs
How Insecure Design Manifests in Feathersjs
Insecure design in Feathersjs applications often stems from developers relying on convention over security, leading to endpoints that expose more functionality than intended. The framework's service-oriented architecture makes it easy to accidentally create endpoints that allow unauthorized access to sensitive data or operations.
A common manifestation is the default service configuration that exposes all CRUD operations without proper authorization checks. Consider a user service that looks like this:
const userSchema = new Schema({
email: String,
password: String,
role: String,
isAdmin: Boolean
});
const userService = app.service('users');
Without explicit authorization, this service allows any authenticated user to query all users, update any user's role, or even delete accounts. The Feathersjs philosophy of "convention over configuration" means developers might not realize they're exposing dangerous endpoints.
Another Feathers-specific insecure design pattern occurs with relation handling. When using populate or fastJoin, developers might inadvertently expose related data:
const userService = app.service('users');
userService.hooks({
before: {
find: [populate({
schema: {
include: {
service: 'payments',
nameAs: 'payments'
}
}
})]
}
});
This configuration allows any user to see all payment records across the system, not just their own. The fastJoin hook can similarly expose entire data graphs if not properly scoped.
Feathersjs's real-time features via Socket.io or Primus create additional insecure design opportunities. Without proper channel authorization, any connected client can listen to any channel:
app.on('connection', (connection, context) => {
app.channel('authenticated').join(connection);
});
This pattern gives every authenticated user access to all authenticated channel events, potentially exposing sensitive real-time data across tenant boundaries.
Service mixins present another attack surface. The default events mixin might broadcast sensitive operations:
const userService = app.service('users');
userService.on('created', async (data) => {
// No filtering - broadcasts to all connected clients
app.channel('authenticated').send('userCreated', data);
});
Any client can now listen for user creation events and potentially harvest sensitive information from the event payload.
The framework's flexibility with custom methods can also lead to insecure design. Developers might create methods that bypass standard authorization hooks:
const userService = app.service('users');
userService.customMethod = async (id, data, params) => {
// No authorization check - anyone can call this
return await userService.get(id);
};
Without proper security considerations, these custom methods become backdoors that skip the framework's built-in protections.
Feathersjs-Specific Detection
Detecting insecure design in Feathersjs applications requires understanding the framework's architecture and common patterns. Start by examining your service configurations for missing authorization hooks.
Use middleBrick to scan your Feathersjs API endpoints. The scanner will identify services without proper authorization, exposing the following risks:
middlebrick scan https://api.example.com
middleBrick's Feathersjs-specific checks include:
- Authentication bypass detection - identifies endpoints that lack proper auth middleware
- Authorization gap analysis - finds services missing
feathers-authentication-hooksor similar authorization checks - Relation exposure scanning - detects
populateorfastJoinconfigurations that expose related data without filtering - Real-time channel authorization - checks for proper channel filtering and user scoping
- Service method exposure - identifies services with default CRUD operations exposed to unauthorized users
Manual detection should focus on these areas:
// Check for missing authorization hooks
const userService = app.service('users');
console.log('Authorization hooks:', userService.hooks); // Should show auth checks
// Examine service methods
const methods = ['find', 'get', 'create', 'update', 'patch', 'remove'];
methods.forEach(method => {
console.log(`Method ${method} hooks:`, userService._getHooks(method));
});
Review your channels.js configuration for proper filtering:
app.on('connection', (connection, context) => {
const { user } = context;
if (user) {
// Only join channels the user should access
app.channel(`user-${user.id}`).join(connection);
}
});
app.publish((data, hook) => {
// Only publish to authorized channels
if (data.userId === hook.params.user.id) {
return app.channel(`user-${data.userId}`);
}
return [];
});
Test for IDOR (Insecure Direct Object Reference) vulnerabilities by attempting to access resources with different user IDs:
# Try accessing another user's data
curl -H "Authorization: Bearer valid-token" \
https://api.example.com/users/9999
Check your koa or express middleware stack to ensure authentication runs before your Feathers services:
app.use(authentication()); // Must be before services
app.use('/users', userService);
Feathersjs-Specific Remediation
Remediating insecure design in Feathersjs requires implementing proper authorization at the service level. Start by adding authentication and authorization hooks to all services.
For basic CRUD services, use feathers-authentication-hooks to enforce ownership:
const { authenticate, authorize } = require('feathers-authentication-hooks');
const userService = app.service('users');
userService.hooks({
before: {
all: [
authenticate('jwt'), // Require JWT for all operations
authorize({
owning: true, // Only allow users to access their own records
idField: 'id',
ownerField: 'userId'
})
],
find: [
// Allow admins to see all users
context => {
if (context.params.user.role === 'admin') {
return context;
}
// Otherwise, filter to user's own record
context.params.query = { id: context.params.user.id };
return context;
}
]
}
});
For relation handling with populate, implement proper filtering:
const userService = app.service('users');
userService.hooks({
before: {
find: [populate({
schema: {
include: {
service: 'payments',
nameAs: 'payments',
// Only include payments belonging to this user
parentField: 'id',
childField: 'userId'
}
}
})]
}
});
For custom methods, always include authorization checks:
const userService = app.service('users');
userService.customMethod = async (id, data, params) => {
// Verify user has permission
if (params.user.role !== 'admin' && params.user.id !== id) {
throw new Forbidden('Insufficient permissions');
}
return await userService.get(id);
};
Secure your real-time channels by implementing proper filtering:
app.on('connection', (connection, context) => {
const { user } = context;
if (user) {
// Join only channels the user should access
app.channel(`user-${user.id}`).join(connection);
// Join admin channel if user is admin
if (user.role === 'admin') {
app.channel('admins').join(connection);
}
}
});
app.publish((data, hook) => {
// Only publish to the owner or admins
if (data.userId === hook.params.user.id || hook.params.user.role === 'admin') {
return app.channel(`user-${data.userId}`);
}
return [];
});
For service mixins, filter events to prevent data leakage:
const userService = app.service('users');
userService.on('created', async (data) => {
// Only send to the creator and admins
const channels = [
app.channel(`user-${data.id}`),
app.channel('admins')
];
app.channel(channels).send('userCreated', data);
});
Implement proper error handling to avoid information disclosure:
userService.hooks({
error: async (context) => {
if (context.error.name === 'NotFound') {
// Don't reveal whether a record exists
context.result = null;
context.error = null;
}
return context;
}
});
Consider using feathers-permissions for complex authorization scenarios:
const { checkPermissions } = require('feathers-permissions');
userService.hooks({
before: {
find: [
checkPermissions(['users:read:all']),
async (context) => {
if (context.params.user.role === 'admin') {
return context;
}
// Filter to user's own records
context.params.query = { id: context.params.user.id };
return context;
}
]
}
});