Credential Stuffing in Sails with Bearer Tokens
Credential Stuffing in Sails with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated attack where adversaries use stolen username and password pairs to gain unauthorized access to user accounts. In Sails.js applications that rely on Bearer token authentication, this risk is amplified when token issuance does not sufficiently bind the token to the original authentication context. If an API endpoint accepts a Bearer token but does not validate that the token was issued for the presented identity, an attacker can replay a token obtained from another source or from a breached dataset.
Consider a Sails backend that exposes an endpoint like /api/account and validates only the presence of a Bearer token in the Authorization header:
// config/routes.js
module.exports.routes = {
'GET /api/account': 'AccountController.show'
};
// api/controllers/AccountController.js
module.exports.show = function (req, res) {
const auth = req.headers['authorization'];
if (!auth || !auth.startsWith('Bearer ')) {
return res.unauthorized('Missing token');
}
const token = auth.split(' ')[1];
// naive verification: only checks presence, not binding to user
if (!TokenService.isValid(token)) {
return res.unauthorized('Invalid token');
}
return res.ok({ account: TokenService.getPayload(token) });
};
In this pattern, if an attacker obtains a valid Bearer token from a different breach or a reused session, they can use it to access any account the token permits, because the endpoint does not verify that the token’s subject matches the intended resource or that the token was issued through Sails’ own authentication flow. This becomes credential stuffing when attackers couple token replay with lists of breached credentials to probe multiple accounts.
Additionally, Sails endpoints that rely on session cookies but also accept Bearer tokens can create a weak link. If cookie-based sessions are protected with strong anti-CSRF measures but Bearer token endpoints lack equivalent binding, attackers may favor token reuse over cookie-based attacks. Without mechanisms such as per-user token binding, token rotation on login, or strict audience/issuer validation, the API surface remains vulnerable to automated token replay at scale.
For reference, related authentication weaknesses are documented in the OWASP API Top 10 A07, and token handling issues can intersect with Broken Object Level Authorization (BOLA), where object IDs in requests are not properly validated against token scopes.
Bearer Tokens-Specific Remediation in Sails — concrete code fixes
To mitigate credential stuffing in Sails when using Bearer tokens, enforce strict token binding and validation so that each token is explicitly tied to the identity it claims to represent. The following patterns demonstrate secure handling.
1. Validate token binding to the request subject
Ensure the token’s subject (sub claim or equivalent) matches the resource being accessed. For example, if a user requests their own account, compare the token subject to the logged-in user identifier:
// api/controllers/AccountController.js
const jwt = require('jsonwebtoken');
module.exports.show = async function (req, res) {
const auth = req.headers['authorization'];
if (!auth || !auth.startsWith('Bearer ')) {
return res.unauthorized('Missing token');
}
const token = auth.split(' ')[1];
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY || 'your-secret');
} catch (err) {
return res.unauthorized('Invalid token');
}
// binding check: token subject must match requested account id
const accountId = req.param('id');
if (!accountId || decoded.sub !== accountId) {
return res.forbidden('Token subject does not match requested account');
}
const account = await Account.findOne(accountId);
if (!account) {
return res.notFound();
}
return res.ok(account);
};
2. Use short-lived tokens and rotate on privilege changes
Issue short expiration times for Bearer tokens and rotate them upon login or password changes. This limits the window for token reuse in credential stuffing campaigns.
// api/services/TokenService.js
const jwt = require('jsonwebtoken');
module.exports = {
issueToken: function (userId) {
return jwt.sign(
{ sub: userId, iat: Math.floor(Date.now() / 1000) },
process.env.JWT_PRIVATE_KEY || 'your-secret',
{ expiresIn: '15m', issuer: 'sails-app', audience: 'sails-api' }
);
},
verifyToken: function (token) {
return jwt.verify(token, process.env.JWT_PUBLIC_KEY || 'your-secret', {
issuer: 'sails-app',
audience: 'sails-api'
});
}
};
3. Enforce rate limiting and anomaly detection on token usage
Apply rate limits per token or per user to reduce the effectiveness of automated token replay. Combine with monitoring for multiple failed authorization attempts across tokens.
// config/policies.js
module.exports.policies = {
'*': ['rateLimitToken']
};
// api/policies/rateLimitToken.js
const tokenUsage = new Map();
module.exports = async function rateLimitToken(req, res, next) {
const auth = req.headers['authorization'];
if (auth && auth.startsWith('Bearer ')) {
const token = auth.split(' ')[1];
const count = tokenUsage.get(token) || 0;
if (count > 10) { // example threshold
return res.status(429).send('Too many requests');
}
tokenUsage.set(token, count + 1);
setTimeout(() => tokenUsage.set(token, tokenUsage.get(token) - 1), 60_000);
}
return next();
};
By binding tokens to subjects, shortening lifetimes, and monitoring usage, Sails APIs can significantly reduce the impact of credential stuffing when Bearer tokens are involved.