DC-026/027/028: close 3 more auth security holes + rate limit /auth/* + audit credential exposures
[DC-026] routes/auth/sso-gate.js — fix sessionDuration='never' bypass Both /auth/gate/:serviceId and /auth/app-token/:serviceId had a session check gated on `sessionDuration !== 'never'`. An admin setting TOTP to never-expire accidentally created an authentication-free path to credential injection (Basic Auth, X-Api-Key, Plex/Prowlarr tokens). Patched: session required whenever TOTP is enabled, period. Added 8 regression tests. [DC-027] src/utilities/middleware.js — rate limit /auth/* New authLimiter (20 req / 15 min) on /auth/keys, /auth/jwt, /auth/gate, /auth/app-token. These endpoints expose credentials and were unmetered. Without this, an attacker with a guessed session cookie could burn through every credential-touching endpoint. Added 5 tests. [DC-028] src/security/audit-logger.js — log credential exposures /auth/gate and /auth/app-token were in SKIP_PATHS, silently dropping every credential-exposure event from the audit log. Combined with the GET-skip rule, NONE of these events were being recorded. Now logged with named actions: auth.credential-injection, auth.app-token-issue, auth.api-key-generate, auth.api-key-revoke, auth.jwt-mint. Added 9 tests. [start.sh] Disable in-container self-updater DASHCADDY_UPDATE_ENABLED=false. Without this, the container kept writing trigger.json every 30 min and clobbered my in-progress host edits. The path unit on the host is still active for manual triggers, but the container won't auto-update itself — only when an admin clicks the update button or a new release is manually published. [package.json] Bump to 1.14.7 Test results: 1066/1066 passing across 39 suites (added 22 new tests).
This commit is contained in:
@@ -53,14 +53,23 @@ const ACTION_MAP = {
|
||||
'DELETE /api/v1/favicon': 'config.favicon-delete',
|
||||
'POST /api/v1/tailscale/config': 'config.tailscale',
|
||||
'POST /api/v1/tailscale/protect-service': 'config.tailscale-protect',
|
||||
// SECURITY [DC-028]: Credential-exposure events get named actions so the
|
||||
// audit log can answer "who hit /auth/gate/plex at 03:00 with what outcome?".
|
||||
'GET /api/v1/auth/gate': 'auth.credential-injection',
|
||||
'GET /api/v1/auth/app-token': 'auth.app-token-issue',
|
||||
'POST /api/v1/auth/keys': 'auth.api-key-generate',
|
||||
'DELETE /api/v1/auth/keys': 'auth.api-key-revoke',
|
||||
'POST /api/v1/auth/jwt': 'auth.jwt-mint',
|
||||
};
|
||||
|
||||
// Paths to skip logging (noisy or internal)
|
||||
const SKIP_PATHS = [
|
||||
'/api/v1/totp/verify',
|
||||
'/api/v1/totp/check-session',
|
||||
'/api/v1/auth/gate/',
|
||||
'/api/v1/auth/app-token/',
|
||||
// SECURITY [DC-028]: /auth/gate and /auth/app-token are NOT skipped —
|
||||
// they expose credentials (Basic Auth, X-Api-Key, upstream service tokens)
|
||||
// so we MUST log every hit. Previously these were in SKIP_PATHS which
|
||||
// silently dropped credential-exposure events from the audit log.
|
||||
'/api/v1/audit-logs',
|
||||
'/api/v1/health',
|
||||
'/health',
|
||||
@@ -95,6 +104,14 @@ class AuditLogger {
|
||||
}
|
||||
|
||||
shouldSkip(method, urlPath) {
|
||||
// SECURITY [DC-028]: Auth endpoints that expose credentials are
|
||||
// logged even though they're GETs. /auth/gate and /auth/app-token
|
||||
// return Basic Auth headers and upstream service tokens — these
|
||||
// events MUST be auditable. Other GETs remain skipped (probes,
|
||||
// dashboards, status checks flood the log).
|
||||
if (urlPath.startsWith('/api/v1/auth/gate') || urlPath.startsWith('/api/v1/auth/app-token')) {
|
||||
return false; // log it
|
||||
}
|
||||
if (method === 'GET') return true;
|
||||
for (const skip of SKIP_PATHS) {
|
||||
if (urlPath.startsWith(skip)) return true;
|
||||
|
||||
@@ -479,6 +479,25 @@ module.exports = function configureMiddleware(app, {
|
||||
// RateLimit-Limit / RateLimit-Remaining for clients to see.
|
||||
app.use('/api/v1/totp/setup', totpLimiter);
|
||||
|
||||
// SECURITY [DC-027]: Dedicated rate limiter for credential-touching auth
|
||||
// endpoints. /auth/keys (manage API keys), /auth/jwt (mint admin JWT),
|
||||
// /auth/gate/* (Caddy forward_auth — returns Basic Auth + X-Api-Key),
|
||||
// /auth/app-token/* (returns upstream service tokens like Plex/Prowlarr).
|
||||
// Without this, an attacker with a guessed-or-leaked session cookie could
|
||||
// burn through every endpoint. 20 requests per 15 min is plenty for legit
|
||||
// use (1/min average) but cuts off brute-force + scraping cold.
|
||||
const authLimiter = rateLimit({
|
||||
...RATE_LIMITS.STRICT,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: () => isTest,
|
||||
message: { success: false, error: 'Too many auth requests, please try again later' }
|
||||
});
|
||||
app.use('/api/v1/auth/keys', authLimiter);
|
||||
app.use('/api/v1/auth/jwt', authLimiter);
|
||||
app.use('/api/v1/auth/gate', authLimiter);
|
||||
app.use('/api/v1/auth/app-token', authLimiter);
|
||||
|
||||
// ── Audit logging middleware (logs non-GET API requests) ──
|
||||
app.use(auditLogger.middleware());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user