HIGH sandbox escapefeathersjsbasic auth

Sandbox Escape in Feathersjs with Basic Auth

Sandbox Escape in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability

A sandbox escape in a Feathersjs application using Basic Authentication can occur when authorization boundaries between tenant or user contexts are not enforced, allowing an authenticated request to access or modify data belonging to another context. Feathersjs services are typically defined as classes with find, get, create, update, and patch methods, and it is common to scope queries using hooks that inject a query filter such as { userId: this.user.id }. If the Basic Auth credentials are validated but the resulting user identity is not consistently applied as a hard filter across services, an attacker may manipulate parameters in a way that bypasses intended isolation.

Consider a multi-tenant API where each tenant is identified by a accountId. A typical Feathers hook might attach the authenticated user and their tenant to the params object:

// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');

module.exports = function authenticateBasic() {
  return async context => {
    const { username, password } = context.params.query;
    // Validate credentials against a user store; for example:
    const user = await verifyBasicCredentials(username, password);
    if (!user) {
      throw new AuthenticationError('Invalid credentials');
    }
    // Attach user and tenant to context for downstream services
    context.params.user = user;
    context.params.accountId = user.accountId;
    return context;
  };
};

If a service implementation does not enforce accountId in its data access layer, an attacker could craft a request that includes an explicit accountId in the query or body, and if the service merges user-supplied filters with the default scope, they may retrieve records outside their tenant. For example, a GET to /reports?accountId=otherAccount might return data belonging to another tenant if the service does not override the query to exclude foreign accountId values.

Additionally, Feathers allows custom query parameters that can be inadvertently exposed to mass assignment or injection if the service schema does not restrict them. An attacker might probe open endpoints using Basic Auth and attempt parameter pollution or prototype pollution to escape the logical sandbox. Because Basic Auth transmits credentials in a reversible format (base64), it must always be used over TLS to prevent interception; however, the vulnerability here is not the transport but the missing enforcement of tenant isolation at the service or model layer.

In a black-box scan, such misconfigurations can be detected by submitting authenticated requests with altered identifiers and observing whether data from other sandbox boundaries is returned. The presence of inconsistent scoping, missing default filters, or overly permissive mongoose (or other ORM) query merging can amplify the risk. Remediation focuses on ensuring that every data access path within Feathers services applies a hard filter based on the authenticated identity, preventing cross-tenant reads or writes regardless of client-supplied parameters.

Basic Auth-Specific Remediation in Feathersjs — concrete code fixes

Remediation centers on enforcing tenant or user isolation at the service layer and ensuring that Basic Auth credentials are used solely for identity derivation, not for query construction that can be overridden. Always apply a default filter in the service or hook layer so that client-supplied query parameters cannot override the security boundary.

Example of a secure Feathers service definition that scopes all queries to the authenticated user’s tenant:

// src/services/reports/reports.class.js
const { iff, isProvider } = require('feathers-hooks-common');

class ReportsService {
  async find(params) {
    // The accountId must come from the authenticated context, not from params.query
    const { accountId } = params.accountId;
    // Apply a hard filter that cannot be overridden by the client
    params.query = { ...params.query, accountId };
    return this.app.service('reports').Model.find(params.query);
  }

  async get(id, params) {
    const { accountId } = params.accountId;
    const record = await this.app.service('reports').Model.findById(id);
    if (!record || record.accountId !== accountId) {
      throw new NotFound('Report not found or access denied');
    }
    return record;
  }
}

module.exports = function (app) {
  const options = {
    Model: app.get('reportsModel'),
    paginate: app.get('paginate')
  };
  app.use('/reports', new ReportsService(options));
  const service = app.service('reports');
  service.hooks({
    before: {
      all: [
        // Ensure accountId is injected and cannot be overridden
        async context => {
          if (context.params && context.params.accountId) {
            context.params.query = { ...context.params.query, accountId: context.params.accountId };
          }
          return context;
        }
      ],
      find: [],
      get: [],
      create: [],
      update: [],
      patch: [],
      remove: []
    }
  });
};

In this approach, the hook that attaches accountId to params runs before any service method, and the service method explicitly merges the hard filter into the query. This prevents a client from supplying accountId in the request and overriding the security boundary.

When using Basic Auth, ensure that credentials are verified over HTTPS and that the authenticated identity is mapped to a minimal set of claims. Avoid passing raw credentials to downstream services or logging them. A minimal hook to validate Basic Auth and reject unsafe requests might look like:

// src/hooks/basic-auth-secure.js
const { AuthenticationError } = require('@feathersjs/errors');
const basicAuth = require('basic-auth');

module.exports = function secureBasicAuth() {
  return async context => {
    const req = context.req;
    const user = basicAuth(req);
    if (!user || !user.name || !user.pass) {
      throw new AuthenticationError('Authentication required');
    }
    const verified = await verifyBasicCredentials(user.name, user.pass);
    if (!verified) {
      throw new AuthenticationError('Invalid credentials');
    }
    // Attach only necessary claims; do not forward raw credentials
    context.params.authUser = {
      id: verified.id,
      accountId: verified.accountId,
      roles: verified.roles || []
    };
    context.params.accountId = verified.accountId;
    return context;
  };
};

Combine this with service hooks that reject any client-supplied accountId in queries to finalize the sandbox boundary. Regularly review service definitions for unintentional parameter merging and ensure that mass assignment protections are enabled for your ORM or adapter.

Frequently Asked Questions

Can an authenticated user exploit parameter pollution to bypass tenant isolation in Feathersjs with Basic Auth?
Yes, if the service merges client-supplied query parameters with default filters without hard enforcement, an authenticated user can supply an alternate accountId and read or write across tenant boundaries. Remediate by applying a server-side filter that overrides any client-provided accountId.
Is Basic Auth inherently insecure in Feathersjs compared to other authentication methods?
Basic Auth is not inherently insecure if used over TLS and paired with strict server-side scoping. The risk in Feathersjs arises from inconsistent enforcement of tenant isolation rather than the authentication mechanism itself; ensure identity-derived claims are applied as immutable filters in every service method.