HIGH broken access controlsailsjavascript

Broken Access Control in Sails (Javascript)

Broken Access Control in Sails with Javascript — how this specific combination creates or exposes the vulnerability

Broken Access Control is an OWASP API Top 10 category that commonly manifests in Sails.js applications written in JavaScript when authorization checks are missing, incomplete, or bypassed at the controller or policy layer. Sails provides a policy system that can run synchronously or asynchronously, but if policies are not applied consistently to every route that touches sensitive data or administrative actions, an unauthenticated or low-privilege user can reach endpoints that should be restricted.

In a typical Sails app, controllers expose actions that map to routes, and policies are attached either globally, per controller, or per action. If a developer forgets to add an authorization policy to an action, or relies only on frontend UI hiding (security through obscurity), an attacker can call the endpoint directly. For example, a JavaScript controller action that returns user records without checking whether the requesting user has permission to view those records enables IDOR/Insecure Direct Object References. Sails Waterline ORM does not automatically enforce row-level permissions; it is the developer’s responsibility to scope queries to the requesting user or tenant.

Additionally, Sails blueprints can inadvertently expose create, read, update, and delete (CRUD) routes if blueprint actions are enabled globally in config/controllers.js. When combined with weak or missing ownership checks in the model or policy, this allows horizontal privilege escalation where one user can operate on another user’s resources. Vertical privilege escalation can occur if an attacker adds an administrative flag to their payload or reaches admin-only routes because the authorization policy is not enforced for certain HTTP verbs or paths.

Because Sails applications often expose REST-like endpoints via controllers and policies, a missing or incorrect check in a controller method or policy can lead to sensitive data exposure, unauthorized modification, or deletion. For instance, a policy that only checks for authentication (via req.isAuthenticated()) but not for authorization (role or scope validation) satisfies the check but still leaks or mutates data. In JavaScript, this often means using truthy role strings or numeric IDs without verifying hierarchy, tenant, or consent, which can be manipulated if input validation is weak.

When using OpenAPI/Swagger specs with Sails, it is important to ensure that spec-defined security schemes are actually enforced by the runtime controllers and policies. MiddleBrick’s scans compare the declared spec security requirements against runtime behavior; if an endpoint is marked as requiring a scope or role but the Sails controller does not validate it, the scan will flag this as a Broken Access Control finding with remediation guidance to add explicit authorization checks.

Javascript-Specific Remediation in Sails — concrete code fixes

Remediation in Sails with JavaScript focuses on explicit authorization in controllers or policies, scoping database queries, and ensuring blueprint routes are not unintentionally exposed. Below are concrete, working examples.

1. Apply a policy and enforce ownership in the controller

Define a policy that checks that the requesting user owns the resource or has the required role. Then attach the policy to the controller action and scope the Waterline query to the user.

// api/policies/ensureOwnership.js
module.exports = async function ensureOwnership(req, res, next) {
  const userId = req.user.id; // authenticated user ID from session or token
  const recordId = req.param('id');

  if (!userId) {
    return res.unauthorized('Authentication required');
  }

  // Fetch the record and verify ownership
  const record = await User.findOne(recordId);
  if (!record) {
    return res.notFound();
  }

  if (record.id !== userId) {
    return res.forbidden('You do not have permission to access this resource');
  }

  // Attach the record to req for downstream use
  req.record = record;
  return next();
};

// config/policies.js
module.exports.policies = {
  UserController: {
    profile: ['ensureOwnership'],
    update: ['ensureOwnership']
  }
};

// api/controllers/UserController.js
module.exports = {
  profile: async function (req, res) {
    // req.record is guaranteed to belong to req.user.id
    return res.ok(req.record);
  },

  update: async function (req, res) {
    const updated = await User.updateOne(req.param('id')).set(req.body);
    if (!updated) {
      return res.notFound();
    }
    return res.ok(updated);
  }
};

2. Scope blueprint routes and disable public access where unnecessary

If you use Sails blueprints, restrict them by disabling actions or applying policies. For sensitive models, disable blueprint actions and implement explicit controller endpoints with proper authorization.

// config/controllers.js
module.exports.controllers = {
  // Disable blueprint actions for the Account model
  blueprints: {
    actions: false,
    rest: false
  }
};

// api/controllers/AccountController.js
module.exports = {
  list: async function (req, res) {
    // Only allow admins to list accounts
    if (!req.user || !['admin'].includes(req.user.role)) {
      return res.forbidden('Insufficient permissions');
    }
    const accounts = await Account.find();
    return res.ok(accounts);
  },

  create: async function (req, res) {
    // Enforce input validation and ownership/role checks
    if (!req.isAuthenticated) {
      return res.unauthorized();
    }
    // Example: regular users can only create their own accounts
    if (req.user.role !== 'admin') {
      req.body.userId = req.user.id;
    }
    const account = await Account.create(req.body).fetch();
    return res.created(account);
  }
};

3. Validate input and avoid insecure direct object references

Always validate parameters and avoid exposing internal IDs directly. Use permalinks or indirect references where appropriate, and enforce scope checks in policies.

// api/policies/requireScope.js
module.exports = async function requireScope(req, res, next) {
  const allowedScopes = req.user.scopes || [];
  const required = req.options.requiredScopes;

  if (!required) return next();

  const hasAll = required.every(r => allowedScopes.includes(r));
  if (!hasAll) {
    return res.forbidden('Missing required scope');
  }
  return next();
};

// Example usage in a policy assignment
module.exports.policies = {
  PaymentController: {
    refund: ['requireScope'],
    view: ['requireScope']
  }
};

// In a route config or controller, define requiredScopes
// This can be read by MiddleBrick to compare spec expectations vs actual enforcement

By combining explicit policies, scoped queries, and careful blueprint configuration, you mitigate Broken Access Control in Sails JavaScript applications. Regular scans with tools like MiddleBrick help ensure declared security requirements are reflected in runtime behavior.

Frequently Asked Questions

How does MiddleBrick detect Broken Access Control in a Sails JavaScript API?
MiddleBrick compares the declared security requirements in your OpenAPI/Swagger spec (e.g., required roles or scopes) against the runtime behavior of your Sails controllers and policies. If an endpoint should require a role or scope but the Sails code does not enforce it, MiddleBrick reports a Broken Access Control finding with remediation steps.
Can MiddleBrick fix Broken Access Control automatically in Sails apps?
MiddleBrick detects and reports Broken Access Control findings with remediation guidance, but it does not automatically fix or patch your Sails JavaScript code. Developers must implement explicit authorization checks and scoped queries based on the provided guidance.