Auth Bypass in Sails with Api Keys
Auth Bypass in Sails with Api Keys — how this specific combination creates or exposes the vulnerability
Sails is a Node.js web framework that encourages convention-over-configuration and rapid development. When using API keys for authentication, developers often implement them as simple header checks without enforcing strict scoping, validation, or binding to a user context. An Auth Bypass occurs when a request that should require valid authentication is accepted because the API key check is incomplete, misapplied, or bypassed by other code paths.
In Sails, this commonly happens when policies or custom middleware check for an API key only on certain controllers or actions, leaving administrative or legacy endpoints unprotected. For example, a policy might verify req.headers['x-api-key'] on user profile endpoints but omit it for administrative controllers, enabling an unauthenticated actor to call those endpoints directly. Additionally, if API keys are accepted via query parameters or headers without constant-time comparison, attackers may exploit timing differences to infer valid keys.
Another vector specific to Sails involves Waterline ORM associations. If an endpoint uses a key to identify a parent record but fails to enforce ownership or scope checks on related models, an attacker can traverse relationships to access data belonging to other users. This intersects with BOLA/IDOR when a key grants access to a collection but does not ensure that the requesting actor is authorized for the specific resource instance.
During a black-box scan, middleBrick tests unauthenticated endpoints that expect API keys and checks whether sensitive operations or data exposure are possible without valid credentials. It also examines OpenAPI specifications to see whether key-required paths are properly defined and cross-references runtime behavior to detect mismatches between documented and actual authentication requirements.
Api Keys-Specific Remediation in Sails — concrete code fixes
Remediation focuses on consistent policy enforcement, safe key transmission, and strict validation within Sails controllers and policies. Below are concrete, working examples to harden API key usage.
1. Global policy to require and validate API keys
Create a policy that checks for a server-side stored API key and uses constant-time comparison to avoid timing attacks.
// api/policies/requireApiKey.js
const crypto = require('crypto');
module.exports = async function requireApiKey(req, res, next) {
const requestKey = req.headers['x-api-key'];
const serverKey = sails.config.custom.apiKeys.public; // stored securely, e.g., in environment variables
if (!requestKey) {
return res.unauthorized('Missing API key');
}
const isValid = crypto.timingSafeEqual(
Buffer.from(requestKey),
Buffer.from(serverKey)
);
if (!isValid) {
return res.forbidden('Invalid API key');
}
return next();
};
Register this policy in config/policies.js to apply globally or to specific controllers:
module.exports.policies = {
'*': 'requireApiKey',
'admin/*': 'requireApiKey',
'user/profile': 'requireApiKey'
};
2. Key binding to user context to prevent horizontal elevation
Ensure API keys are associated with a user or scope and validated against the requested resource. For example, a user endpoint should verify that the key’s owner matches the requested user ID.
// api/controllers/UserController.js
module.exports = {
async me(req, res) {
const apiKey = req.headers['x-api-key'];
const user = await User.findOne({ apiKey }).meta({ fetch: 'user' });
if (!user) {
return res.unauthorized('Invalid key');
}
return res.ok(user);
}
};
3. Avoid exposing keys in URLs and logs
Prefer headers over query parameters, and ensure keys are not logged by Sails. Configure transports appropriately and avoid printing req.headers in logs.
// config/log.js
module.exports.log = {
level: 'warn',
// Ensure API key headers are not inadvertently serialized in logs
custom: undefined
};
4. Combine with ownership checks for sensitive actions
For endpoints that act on specific resources, add explicit checks that the authenticated key has rights to that resource.
// api/controllers/PostController.js
module.exports = {
async destroy(req, res) {
const { id } = req.params.all();
const apiKey = req.headers['x-api-key'];
const post = await Post.findOne(id);
if (!post) {
return res.notFound();
}
const user = await User.findOne({ apiKey });
if (!user || post.userId !== user.id) {
return res.forbidden('Not authorized');
}
await Post.destroyOne(id);
return res.noContent();
}
};Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |