HIGH insecure designfeathersjs

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-hooks or similar authorization checks
  • Relation exposure scanning - detects populate or fastJoin configurations 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;
      }
    ]
  }
});

Frequently Asked Questions

How does Feathersjs's default configuration contribute to insecure design?
Feathersjs's convention-over-configuration approach means services are created with all CRUD operations exposed by default. Without explicit authorization hooks, any authenticated user can access all service methods, potentially exposing sensitive data or allowing privilege escalation through operations like role updates or account deletions.
Can middleBrick scan my Feathersjs API if it's behind authentication?
Yes, middleBrick can scan authenticated Feathersjs APIs. You can provide authentication credentials (API key, JWT, etc.) in the scan configuration. The scanner will authenticate and then test the authenticated attack surface, identifying insecure design patterns that might be hidden behind authentication but still expose excessive permissions.