DC-022: close 3 TOTP auth security holes
1. /totp/recovery-info: was PUBLIC, leaking TOTP configuration status to unauthenticated attackers. Now requires valid session (401 otherwise). 2. /totp/check-session: had an unconditional bypass that returned authenticated:true whenever totpConfig.enabled was false. This let anyone reach authenticated endpoints without credentials. Now throws AuthenticationError instead. 3. /totp/setup: was unmetered despite generating secrets. Added 3/hour per-IP rate limit in addition to the existing global 10/15min limiter. All changes verified live via https://status.sami: - recovery-info unauth → 401 [DC-110] (was 200) - check-session no cookie → 401 TOTP protection required (was 200) - 4th setup attempt → 429 [DC-429]
This commit is contained in:
@@ -37,7 +37,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
||||
});
|
||||
}, 'totp-config-get'));
|
||||
|
||||
// Recovery diagnostic (public, no auth required).
|
||||
// Recovery diagnostic.
|
||||
//
|
||||
// Returns information a locked-out user needs to choose a recovery path:
|
||||
// - whether TOTP is configured at all (isSetUp)
|
||||
@@ -51,7 +51,16 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
||||
// 'corrupt' — entry exists but value is malformed
|
||||
//
|
||||
// This route never returns the secret itself — only metadata about it.
|
||||
// AUTH GATE: requires a valid session. Was previously public, which let
|
||||
// unauthenticated attackers probe TOTP state on a target server.
|
||||
router.get('/totp/recovery-info', asyncHandler(async (req, res) => {
|
||||
if (!ctx.session.isValid(req)) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: '[DC-110] Authentication required',
|
||||
code: 'DC-401'
|
||||
});
|
||||
}
|
||||
if (!ctx.totpConfig.isSetUp) {
|
||||
return res.json({
|
||||
success: true,
|
||||
@@ -97,8 +106,27 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
||||
});
|
||||
}, 'totp-recovery-info'));
|
||||
|
||||
// Rate limiter for /totp/setup — prevents QR endpoint abuse / secret enumeration.
|
||||
// Per-IP sliding window. Defaults: 3 attempts per hour.
|
||||
const _setupAttempts = router._setupAttempts || (router._setupAttempts = new Map());
|
||||
const SETUP_LIMIT = 3;
|
||||
const SETUP_WINDOW_MS = 60 * 60 * 1000;
|
||||
|
||||
// Generate new TOTP secret + QR code
|
||||
router.post('/totp/setup', asyncHandler(async (req, res) => {
|
||||
const ip = (ctx.session.getClientIP ? ctx.session.getClientIP(req) : (req.ip || req.socket?.remoteAddress || 'unknown'));
|
||||
const now = Date.now();
|
||||
const recent = (_setupAttempts.get(ip) || []).filter(t => now - t < SETUP_WINDOW_MS);
|
||||
if (recent.length >= SETUP_LIMIT) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
error: 'Too many setup attempts. Try again in an hour.',
|
||||
code: 'DC-429'
|
||||
});
|
||||
}
|
||||
recent.push(now);
|
||||
_setupAttempts.set(ip, recent);
|
||||
|
||||
const { authenticator } = require('otplib');
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
@@ -202,8 +230,14 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
|
||||
if (!ctx.totpConfig.enabled || ctx.totpConfig.sessionDuration === 'never') {
|
||||
return res.status(200).json({ authenticated: true });
|
||||
// Bypass REMOVED for security: the previous code returned authenticated:true
|
||||
// whenever totpConfig.enabled was false or sessionDuration was 'never'. That
|
||||
// allowed anyone reaching the API to bypass auth entirely. The only safe
|
||||
// behavior is to require a valid session OR to throw AuthenticationError.
|
||||
// Operators wanting development convenience should enable TOTP locally or
|
||||
// bind the service to 127.0.0.1 only.
|
||||
if (!ctx.totpConfig.enabled) {
|
||||
throw new AuthenticationError('[DC-110] TOTP protection required');
|
||||
}
|
||||
|
||||
const valid = ctx.session.isValid(req);
|
||||
|
||||
Reference in New Issue
Block a user