HIGH cwe 284sails

CWE-284 in Sails

How Cwe 284 Manifests in Sails

Cwe 284 (Improper Access Control) in Sails.js applications typically manifests through misconfigured policies, incorrect model permissions, and flawed authorization logic. Sails' flexible policy system, while powerful, can create security gaps when developers misunderstand how policies apply across different routes and actions.

The most common pattern involves developers creating a policy that checks if a user is authenticated, but failing to verify whether that user actually owns or has permission to access the specific resource. For example:

// policies/isAuthenticated.js
module.exports = async (req, res, next) => {
  if (req.session.userId) {
    return next();
  }
  return res.forbidden();
};

This policy only verifies authentication, not authorization. A user could access /api/users/999 and see another user's data simply by changing the ID in the URL.

Sails' blueprint routes compound this issue. When blueprint actions are enabled, Sails automatically generates RESTful routes for models. If you have a User model and don't properly restrict blueprint actions:

// config/blueprints.js
module.exports.blueprints = {
  actions: true,
  rest: true,
  shortcuts: true
};

This exposes find, findOne, create, update, and destroy actions without any access controls. An attacker can iterate through user IDs or access admin endpoints directly.

Another common manifestation occurs with Sails' populate method. Developers often forget to filter associated records:

// api/controllers/UserController.js
async find(req, res) {
  const user = await User.findOne(req.session.userId)
    .populate('posts');
  return res.json(user);
}

If the posts association doesn't filter by author, a user could see posts from other users if they know the post IDs.

Dynamic scoping in Sails Waterline ORM can also introduce Cwe-284 vulnerabilities. When using find with dynamic criteria:

const userId = req.param('userId');
const posts = await Post.find({ author: userId });

If userId comes from user input without validation, an attacker could access any user's posts by manipulating the parameter.

Sails' policy hierarchy adds another layer of complexity. Policies can be applied at the controller level, action level, or route level, and the order matters. A common mistake is applying a restrictive policy at the controller level but forgetting that blueprint actions might bypass it:

// config/policies.js
module.exports.policies = {
  UserController: 'isAuthenticated',
  * : 'isAuthenticated' // global policy
};

This setup might seem secure, but blueprint routes could still be accessible if not explicitly restricted.

Sails-Specific Detection

Detecting Cwe-284 in Sails applications requires examining both the codebase and runtime behavior. Start with a comprehensive policy audit:

// Check all policies for proper authorization logic
module.exports = {
  '*': [ 'isAuthenticated' ],
  UserController: {
    'find': [ 'isAuthenticated', 'checkOwnership' ],
    'update': [ 'isAuthenticated', 'checkOwnership' ],
    'destroy': [ 'isAdmin' ]
  }
};

Look for policies that only check authentication (req.session.userId) without verifying resource ownership or permissions.

Examine blueprint configuration carefully. In config/blueprints.js, ensure that blueprint actions are only enabled where absolutely necessary:

module.exports.blueprints = {
  actions: true,     // Only enable if you need custom actions
  rest: false,      // Disable REST blueprint routes
  shortcuts: false  // Disable shortcuts for better security
};

Audit all controller actions for proper authorization checks. Use a static analysis tool or manual review to verify that every data access operation includes ownership verification:

// Vulnerable pattern
async show(req, res) {
  const post = await Post.findOne(req.params.id);
  return res.json(post); // No ownership check!
}

// Secure pattern
async show(req, res) {
  const post = await Post.findOne({
    id: req.params.id,
    author: req.session.userId
  });
  if (!post) return res.notFound();
  return res.json(post);
}

middleBrick's API security scanner can detect these vulnerabilities by testing authenticated endpoints with different user contexts. The scanner will attempt to access resources with IDs that don't belong to the authenticated user, identifying missing authorization checks.

Run middleBrick from your terminal to scan your Sails API:

npx middlebrick scan https://yourapp.com/api

# Or integrate into CI/CD
npx middlebrick scan --fail-below B

The scanner tests for BOLA (Broken Object Level Authorization) by systematically modifying resource IDs in authenticated requests and checking if access is properly restricted.

Check your Sails models for proper associations and filters. Waterline associations should include filters to prevent unauthorized data access:

// api/models/Post.js
module.exports = {
  attributes: {
    title: { type: 'string' },
    content: { type: 'string' },
    author: {
      model: 'User',
      required: true
    }
  },
  
  beforeFind: async (criteria, proceed) => {
    if (criteria.author === undefined) {
      criteria.author = req.session.userId;
    }
    return proceed();
  }
};

Review your Sails configuration for CORS settings that might expose endpoints to unauthorized origins. In config/cors.js, restrict origins to only trusted domains:

module.exports.cors = {
  origin: process.env.CORS_ORIGIN || 'https://yourapp.com',
  credentials: true
};

Sails-Specific Remediation

Remediating Cwe-284 in Sails requires a multi-layered approach. Start by implementing a robust authorization framework using Sails policies and custom middleware.

Create a comprehensive ownership check policy:

// api/policies/checkOwnership.js
module.exports = async (req, res, next) => {
  const model = req.options.model;
  const id = req.params.id;
  
  if (!model || !id) return next();
  
  const Model = sails.models[model];
  if (!Model) return res.serverError('Unknown model');
  
  let record;
  try {
    record = await Model.findOne({
      id: id,
      author: req.session.userId
    });
  } catch (err) {
    return res.serverError(err);
  }
  
  if (!record) return res.forbidden('Access denied');
  req.record = record;
  return next();
};

Apply this policy to all resource-modifying actions:

// config/policies.js
module.exports.policies = {
  UserController: {
    'find': [ 'isAuthenticated' ],
    'update': [ 'isAuthenticated', 'checkOwnership' ],
    'destroy': [ 'isAuthenticated', 'checkOwnership' ]
  },
  PostController: {
    'create': [ 'isAuthenticated' ],
    'update': [ 'isAuthenticated', 'checkOwnership' ],
    'destroy': [ 'isAuthenticated', 'checkOwnership' ]
  }
};

For admin-level access, create a separate policy:

// api/policies/isAdmin.js
module.exports = async (req, res, next) => {
  const user = await User.findOne(req.session.userId);
  if (!user || !user.isAdmin) {
    return res.forbidden('Admin access required');
  }
  return next();
};

Modify your blueprint configuration to disable automatic REST routes and use custom actions instead:

// config/blueprints.js
module.exports.blueprints = {
  actions: true,
  rest: false,
  shortcuts: false,
  pluralize: false
};

Implement controller actions with built-in authorization:

// api/controllers/PostController.js
module.exports = {
  async find(req, res) {
    const posts = await Post.find({
      author: req.session.userId,
      sort: 'createdAt DESC'
    });
    return res.json(posts);
  },
  
  async findOne(req, res) {
    const post = await Post.findOne({
      id: req.params.id,
      author: req.session.userId
    });
    if (!post) return res.notFound();
    return res.json(post);
  },
  
  async create(req, res) {
    const postData = {
      ...req.body,
      author: req.session.userId
    };
    const post = await Post.create(postData).fetch();
    return res.status(201).json(post);
  },
  
  async update(req, res) {
    const post = await Post.updateOne({
      id: req.params.id,
      author: req.session.userId
    }, req.body);
    if (!post) return res.notFound();
    return res.json(post);
  },
  
  async destroy(req, res) {
    const deleted = await Post.destroyOne({
      id: req.params.id,
      author: req.session.userId
    });
    if (!deleted) return res.notFound();
    return res.json({ message: 'Post deleted' });
  }
};

Use Sails' lifecycle callbacks to enforce authorization at the model level:

// api/models/Post.js
module.exports = {
  attributes: {
    title: { type: 'string' },
    content: { type: 'string' },
    author: {
      model: 'User',
      required: true
    }
  },
  
  beforeUpdate: async (values, proceed) => {
    if (values.id) {
      const post = await Post.findOne(values.id);
      if (post.author !== req.session.userId) {
        return proceed(new Error('Unauthorized'));
      }
    }
    return proceed();
  },
  
  beforeDestroy: async (criteria, proceed) => {
    const post = await Post.findOne(criteria);
    if (!post || post.author !== req.session.userId) {
      return proceed(new Error('Unauthorized'));
    }
    return proceed();
  }
};

Implement role-based access control using Sails configuration:

// config/rbac.js
module.exports.rbac = {
  roles: {
    user: {
      can: ['read:own', 'update:own', 'delete:own']
    },
    admin: {
      can: ['read:all', 'update:any', 'delete:any', 'create:any']
    }
  },
  permissions: {
    'read:own': (req, record) => record.author === req.session.userId,
    'update:own': (req, record) => record.author === req.session.userId,
    'delete:own': (req, record) => record.author === req.session.userId,
    'read:all': () => true,
    'update:any': () => true,
    'delete:any': () => true,
    'create:any': () => true
  }
};

Create a permission checking utility:

// api/hooks/rbac/index.js
module.exports = function (sails) {
  return {
    initialize: async (cb) => {
      sails.permissionCheck = (action, record) => {
        const user = req.session.user;
        if (!user) return false;
        
        const role = user.isAdmin ? 'admin' : 'user';
        const rbac = sails.config.rbac;
        
        const permission = rbac.permissions[action];
        if (!permission) return false;
        
        return permission(req, record);
      };
      return cb();
    }
  };
};

Frequently Asked Questions

How does middleBrick detect Cwe-284 vulnerabilities in Sails applications?
middleBrick performs black-box scanning by sending authenticated requests with manipulated resource IDs to test for Broken Object Level Authorization. The scanner systematically modifies IDs in API requests and verifies whether the application properly restricts access to resources owned by the authenticated user. It also analyzes OpenAPI specs to identify endpoints that lack proper authorization controls and tests for common Sails-specific patterns like blueprint route exposure and improper policy application.
What's the difference between authentication and authorization in Sails, and why does it matter for Cwe-284?
Authentication verifies who a user is (typically through sessions or JWTs), while authorization determines what they can access. A common Cwe-284 vulnerability occurs when developers implement authentication policies that only check if a user is logged in, but fail to verify whether that user owns or has permission to access specific resources. In Sails, this often happens with blueprint routes or custom actions that don't include ownership checks, allowing authenticated users to access any resource by simply changing the ID in the URL.