Broken Access Control in Sails with Mongodb
Broken Access Control in Sails with Mongodb — how this specific combination creates or exposes the vulnerability
Broken Access Control in a Sails application backed by MongoDB typically arises when authorization checks are missing, incomplete, or bypassed at the data layer. Sails is an MVC framework that encourages rapid development, but if policies and model logic do not enforce per-request permissions, attackers can manipulate parameters to access or modify other users’ resources.
With MongoDB, the risk is compounded when queries use raw filters built from user-supplied input without validating ownership or tenant context. For example, an endpoint like /user/:id might construct a MongoDB filter as { _id: req.params.id } without confirming that the authenticated user owns that document. If the client supplies a different _id, and the server does not re-check ownership, horizontal privilege escalation occurs.
In Sails, this often maps to BOLA/IDOR findings in scans. A MongoDB-specific factor is the use of ObjectId strings; if the application casts parameters incorrectly or trusts client-provided strings, attackers can use valid ObjectId values that exist but belong to other users. Additionally, Sails Waterline ORM may inadvertently expose fields or allow updates through mass assignment unless attributes are explicitly guarded, enabling privilege escalation or unauthorized property access.
Consider an endpoint that updates a user profile using User.update(req.params.id).set(req.body). If the policy does not scope the update to the authenticated user’s record, an attacker can change any user’s email or role by guessing or iterating over ObjectIds. A related pattern is insecure default scopes: if a Sails model defines a global scope such as owner: req.user.id, but an action overrides or omits it, queries may return or modify records outside the intended scope.
SSRF and external data exposure can also intersect with access control. If a Sails endpoint uses user-supplied input to form MongoDB queries and that input influences network calls (e.g., fetching external metadata), attackers may probe internal services. Data exposure can happen when responses include sensitive fields because the response serializer does not strip them based on role or consent settings.
Mongodb-Specific Remediation in Sails — concrete code fixes
Remediation centers on enforcing ownership checks, using parameterized queries, and avoiding mass assignment. Always resolve the authenticated user ID from the session or token and include it as a mandatory filter condition in every MongoDB query.
Example: safe record retrieval with ownership scoping.
// api/controllers/user/getProfile.js
module.exports = async function (req, res) {
const userId = req.user.id; // authenticated user from session/policy
const paramId = req.params.id;
// Ensure the requested ID matches the authenticated user
if (!paramId || paramId !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await User.findOne({
_id: userId,
isActive: true
});
if (!user) {
return res.notFound();
}
return res.ok(user);
};
Example: safe update with scoped filter to prevent horizontal IDOR.
// api/controllers/user/updateEmail.js
module.exports = async function (req, res) {
const userId = req.user.id;
const { email } = req.body;
const updated = await User.update({
_id: userId,
isActive: true
}).set({
email: email,
updatedAt: new Date()
});
if (!updated.length) {
return res.status(403).json({ error: 'Update not permitted' });
}
return res.ok({ message: 'Email updated' });
};
Example: avoid mass assignment by specifying allowed attributes.
// api/controllers/user/updateSafe.js
const ALLOWED_ATTRIBUTES = ['firstName', 'lastName', 'email'];
module.exports = async function (req, res) {
const userId = req.user.id;
const payload = {};
for (const key of ALLOWED_ATTRIBUTES) {
if (Object.prototype.hasOwnProperty.call(req.body, key)) {
payload[key] = req.body[key];
}
}
const updated = await User.update({
_id: userId
}).set(payload);
return res.ok(updated);
};
For queries that must filter by a non-ownership field (e.g., team ID), use an allowlist approach and validate against a server-side mapping rather than trusting the client.
// api/controllers/record/listByTeam.js
module.exports = async function (req, res) {
const userId = req.user.id;
const teamId = req.param('teamId');
// Verify user-team membership server-side
const membership = await TeamMembership.findOne({
userId: userId,
teamId: teamId
});
if (!membership) {
return res.status(403).json({ error: 'Access denied' });
}
const records = await Record.find({
teamId: teamId,
isArchived: false
});
return res.ok(records);
};
Use MongoDB projections to limit returned fields and avoid accidental data exposure. Combine these practices with Sails policies to ensure every request is re-authenticated and re-authorized, and consider integrating middleBrick to validate that your endpoints consistently enforce access controls across unauthenticated and authenticated attack surfaces.