Credential Stuffing in Koa with Mutual Tls
Credential Stuffing in Koa with Mutual TLS — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated attack where attackers use lists of breached username and password pairs to gain unauthorized access to accounts. In a Koa application that uses Mutual TLS (mTLS), the presence of client certificates adds a strong authentication factor, but it does not inherently prevent credential stuffing at the application layer. If the endpoint accepting client certificates also exposes a username/password login, or if mTLS is used only for some routes while others remain unprotected, the attack surface can persist.
Mutual TLS binds client identity to a certificate, but it does not rate-limit authentication attempts at the HTTP layer. An attacker who possesses valid client certificates can still perform credential stuffing against password-based endpoints, or probe account enumeration vulnerabilities (e.g., different responses for existing vs non-existing users). Moreover, if mTLS is misconfigured to accept any client certificate without validating it against a strict trust store, unauthorized clients might reach the application, bypassing intended access controls. The combination therefore creates a nuanced risk: mTLS secures transport and client identity, but application-level authentication logic must still defend against automated credential submission, session fixation, and user enumeration.
For example, a Koa route that verifies a client certificate and then reads a username from the request body can be targeted with a barrage of password guesses. If the route does not enforce rate limiting, account lockout, or CAPTCHA after repeated failures, the mTLS layer alone cannot stop the abuse. The scanner checks for missing rate limiting on authentication endpoints and flags cases where credentials are accepted despite the presence of mTLS, because the two controls address different parts of the threat model.
Mutual TLS-Specific Remediation in Koa — concrete code fixes
To securely combine Mutual TLS with resilient authentication in Koa, enforce strict client certificate validation, apply rate limiting, and avoid leaking account existence. Below are concrete, working examples that demonstrate a hardened setup.
1. Configure mTLS in Koa with explicit trust store and strict verification
Ensure the server requests and validates client certificates against a known CA, and reject connections where the certificate is missing or invalid.
const Koa = require('koa');
const https = require('https');
const fs = require('fs');
const app = new Koa();
const serverOptions = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
ca: fs.readFileSync('ca-cert.pem'),
requestCert: true,
rejectUnauthorized: true,
};
https.createServer(serverOptions, app.callback()).listen(8443, () => {
console.log('Koa mTLS server listening on https://localhost:8443');
});
2. Extract and validate client certificate details in middleware
After mTLS handshake, inspect the client certificate to enforce additional constraints (e.g., allowed subjects or serial numbers) and tie certificate identity to application identity safely.
app.use(async (ctx, next) => {
const cert = ctx.req.client.verifiedCert;
if (!cert) {
ctx.status = 403;
ctx.body = { error: 'Client certificate required' };
return;
}
// Example: restrict by certificate subject or SAN
const allowedSerials = new Set(['AB:CD:EF:12:34:56']);
if (!allowedSerials.has(cert.serialNumber)) {
ctx.status = 403;
ctx.body = { error: 'Certificate not authorized' };
return;
}
ctx.assert(cert.subject, 400, 'Certificate subject missing');
await next();
});
3. Apply layered defenses on authentication endpoints
Use rate limiting and careful response handling to mitigate credential stuffing regardless of mTLS presence. This prevents attackers from leveraging mTLS-authenticated sessions to brute-force passwords.
const Router = require('koa-router');
const rateLimit = require('koa-rate-limit');
const authRouter = new Router();
// Rate limit login attempts per client certificate identity or IP
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // max 5 attempts per window
keyGenerator: (ctx) => {
// Use certificate serial or IP as key
return ctx.req.client.verifiedCert?.serialNumber || ctx.ip;
},
handler: (ctx) => {
ctx.status = 429;
ctx.body = { error: 'Too many attempts, try again later' };
},
});
authRouter.post('/login', loginLimiter, async (ctx) => {
const { username, password } = ctx.request.body;
// Perform secure password verification (e.g., bcrypt.compare)
// Avoid leaking whether username exists
const user = await getUserByUsername(username);
const validPassword = user && await bcrypt.compare(password, user.passwordHash);
if (!validPassword) {
ctx.status = 401;
ctx.body = { error: 'Invalid credentials' };
return;
}
// Issue session/token
ctx.body = { token: 'secure-session-token' };
});
app.use(authRouter.routes()).use(authRouter.allowedMethods());
4. Defend against user enumeration and ensure safe responses
Return consistent responses for authentication failures and avoid information disclosure that could aid attackers. Combine this with mTLS-bound user records for stronger assurance.
async function getUserByUsername(username) {
// Fetch user record tied to mTLS subject if possible
return db.users.findOne({ username });
}