Api Rate Abuse in Sails with Oauth2
Api Rate Abuse in Sails with Oauth2 — how this specific combination creates or exposes the vulnerability
Rate abuse in Sails when OAuth 2.0 is used typically occurs because rate limits are enforced per client credentials (e.g., client_id or API key) rather than per end-user or per token. OAuth 2.0 access tokens represent an authorization context, but if your Sails API does not track token identity and only applies limits at the client level, a single compromised client credential can generate many tokens and exhaust server-side rate budgets for all users sharing that client.
OAuth 2.0 introduces several flows that affect abuse surface. In the Authorization Code flow, an attacker who steals an authorization code can exchange it for an access token. If your Sails backend issues long-lived tokens and does not enforce per-user rate limits, an attacker can repeatedly exchange stolen codes or rotate client credentials to bypass throttling. With the Client Credentials flow, there is no resource owner context; tokens are tied only to the client. Without additional controls, one client can consume the entire quota, affecting all integrations that rely on the same client registration in Sails.
Attack patterns specific to this setup include token hoarding, where an attacker accumulates tokens to maintain a high request rate, and token rotation, where they swap tokens before a limit is detected. If your Sails application uses public clients (e.g., SPAs) with implicit or hybrid flows, tokens may be exposed in browser history or logs, increasing the risk of token collection and coordinated rate abuse across multiple stolen tokens. Because OAuth 2.0 does not mandate per-token rate limits, Sails developers must implement this behavior explicitly.
During a middleBrick scan, such misconfigurations are reflected in the Rate Limiting check. For example, if endpoints under /api/v1/resource allow high request volumes without validating token identity, the scanner can detect missing per-token throttling and report it as a high-severity finding. The scan also flags endpoints that accept OAuth 2.0 bearer tokens but do not correlate requests to a specific token or user, enabling abuse scenarios that standard IP-based limits cannot prevent. These findings highlight gaps between the intended authorization model and actual enforcement in Sails controllers and policies.
Oauth2-Specific Remediation in Sails — concrete code fixes
To mitigate rate abuse with OAuth 2.0 in Sails, enforce rate limits at the token or user level and bind limits to the OAuth 2.0 context. Below are concrete patterns and code examples you can apply in your Sails app.
1. Token-aware rate limiting with a custom policy
Create an authorization policy that extracts the OAuth 2.0 access token from the Authorization header, validates it, and attaches the associated user or client identity to req. Then apply a token-specific rate limiter. This ensures each token has its own quota.
// api/policies/rate-limit-oauth.js
const rateLimit = require('rate-limiter-flexible');
// Define a key generator that includes the token (or user id)
function getKey(req) {
const auth = req.headers.authorization || '';
const match = auth.match(/^Bearer\s+(\S+)$/i);
const token = match ? match[1] : 'anonymous';
// Prefer user id from decoded token; fallback to token hash
return `oauth2:${token}`;
}
const limiter = new rateLimit.RateLimiterMemory({
points: 100, // requests
duration: 60 // per 60 seconds
});
module.exports = async function rateLimitOauth(req, res, next) {
try {
const key = getKey(req);
await limiter.consume(key);
return next();
} catch (rej) {
return res.status(429).json({ error: 'Too Many Requests', retryIn: rej.msBeforeNext / 1000 });
}
};
Register this policy in config/policies.js and apply it to sensitive endpoints in config/routes.js.
2. OAuth 2.0 protected endpoint example with token introspection
Before applying rate limits, validate the token using your authorization server’s introspection endpoint. This example shows how to integrate introspection in a Sails controller and proceed only if the token is active.
// api/controllers/ResourceController.js
const axios = require('axios');
module.exports = {
async show(req, res) {
const auth = req.headers.authorization || '';
const match = auth.match(/^Bearer\s+(\S+)$/i);
if (!match) {
return res.unauthorized('Missing Bearer token');
}
const token = match[1];
// Introspect token against your OAuth 2.0 provider
try {
const introspection = await axios.post(
'https://auth.example.com/oauth/introspect',
new URLSearchParams({ token }),
{
auth: {
username: 'sails-api',
password: process.env.INTROSPECTION_SECRET
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
);
if (!introspection.data.active) {
return res.unauthorized('Invalid token');
}
// Attach user or client context for rate limiting
req.oauth = {
clientId: introspection.data.client_id,
userId: introspection.data.sub,
scopes: (introspection.data.scope || '').split(' ')
};
// Proceed with business logic
const data = await Resource.findOne(req.param('id'));
return res.ok(data);
} catch (err) {
sails.log.error('Introspection failed', err.response?.data || err.message);
return res.serverError('Unable to validate token');
}
}
};
3. Combine client and user scope limits
In Sails, you can maintain two limits: one per client_id for protection against rogue clients, and one per user_id for protection against abusive users. Use a composite key for stricter control.
// api/policies/composite-oauth-rate-limit.js
const limiterByKey = new rateLimit.RateLimiterMemory({
points: 50, // stricter per-user limit
duration: 60
});
const limiterByClient = new rateLimit.RateLimiterMemory({
points: 500, // higher per-client limit
duration: 60
});
module.exports = async function compositeRateLimit(req, res, next) {
const auth = req.headers.authorization || '';
const match = auth.match(/^Bearer\s+(\S+)$/i);
if (!match) {
return res.unauthorized('Missing Bearer token');
}
const token = match[1];
// Assume you have a helper to fetch token metadata
const { clientId, userId } = await getTokenMetadata(token);
const userKey = `user:${userId}`;
const clientKey = `client:${clientId}`;
try {
await limiterByKey.consume(userKey);
await limiterByClient.consume(clientKey);
return next();
} catch (rej) {
return res.status(429).json({ error: 'Rate limit exceeded', retryIn: (rej.msBeforeNext || 1000) / 1000 });
}
};
4. Enforce HTTPS for token transmission
Ensure your Sails server rejects requests that do not use HTTPS for token exchange. This prevents token leakage that could enable token harvesting and abuse.
// config/env/development.js (example)
module.exports = {
hooks: {
http: false,
grunt: false
},
customMiddleware: (app) => {
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.forbidden('HTTPS required');
}
next();
});
}
};
5. Use scopes to limit impact
When issuing tokens in Sails, restrict scopes to the minimum required. Validate scopes in your policies and reject requests that request excessive permissions for the intended operation.
// api/policies/scope-check.js
module.exports = function scopeCheck(requiredScopes) {
return async function(req, res, next) {
if (!req.oauth || !req.oauth.scopes) {
return res.unauthorized('Insufficient scope');
}
const hasAll = requiredScopes.every(s => req.oauth.scopes.includes(s));
if (!hasAll) {
return res.forbidden('Missing required scope');
}
return next();
};
};
// Usage in routes.js
'GET /api/v1/profile': {
policy: ['scope-check'],
scopeCheck: ['read:profile']
}