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:
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Tests for the audit-logger security fixes [DC-028]:
|
||||
* - /auth/gate and /auth/app-token must NOT be skipped (they expose creds)
|
||||
* - Other GETs remain skipped (probes, dashboards)
|
||||
* - The new credential-injection / app-token-issue actions resolve
|
||||
*
|
||||
* These tests focus on shouldSkip() and resolveAction() in isolation.
|
||||
* The middleware() integration is tested via the integration tests in
|
||||
* routes/auth.*.test.js.
|
||||
*/
|
||||
|
||||
const AuditLogger = require('../src/security/audit-logger');
|
||||
|
||||
// Build a fresh AuditLogger class for testability — the singleton at the
|
||||
// bottom of the module makes testing awkward otherwise.
|
||||
function makeLogger() {
|
||||
// Re-require the module's helpers by extracting its internal functions.
|
||||
// Easier: create an instance and exercise its public methods.
|
||||
const logger = Object.create(AuditLogger);
|
||||
return logger;
|
||||
}
|
||||
|
||||
describe('AuditLogger [DC-028] shouldSkip', () => {
|
||||
// Resolve via instance
|
||||
const logger = makeLogger();
|
||||
|
||||
test('skips normal GETs (probes, dashboards)', () => {
|
||||
expect(logger.shouldSkip('GET', '/api/v1/services')).toBe(true);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/config')).toBe(true);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/monitoring/stats')).toBe(true);
|
||||
expect(logger.shouldSkip('GET', '/health')).toBe(true);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/health')).toBe(true);
|
||||
});
|
||||
|
||||
test('skips /totp/verify and /totp/check-session (noisy)', () => {
|
||||
expect(logger.shouldSkip('GET', '/api/v1/totp/verify')).toBe(true);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/totp/check-session')).toBe(true);
|
||||
expect(logger.shouldSkip('POST', '/api/v1/totp/verify')).toBe(true);
|
||||
});
|
||||
|
||||
test('does NOT skip /auth/gate (security: credentials exposed)', () => {
|
||||
expect(logger.shouldSkip('GET', '/api/v1/auth/gate/plex')).toBe(false);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/auth/gate/jellyfin')).toBe(false);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/auth/gate/sonarr')).toBe(false);
|
||||
});
|
||||
|
||||
test('does NOT skip /auth/app-token (security: tokens issued)', () => {
|
||||
expect(logger.shouldSkip('GET', '/api/v1/auth/app-token/plex')).toBe(false);
|
||||
expect(logger.shouldSkip('GET', '/api/v1/auth/app-token/jellyfin')).toBe(false);
|
||||
});
|
||||
|
||||
test('does NOT skip POST/PUT/DELETE on other routes (normal)', () => {
|
||||
expect(logger.shouldSkip('POST', '/api/v1/services')).toBe(false);
|
||||
expect(logger.shouldSkip('PUT', '/api/v1/services/abc')).toBe(false);
|
||||
expect(logger.shouldSkip('DELETE', '/api/v1/auth/keys/xyz')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AuditLogger [DC-028] resolveAction', () => {
|
||||
const logger = makeLogger();
|
||||
|
||||
test('credential-injection resolves for /auth/gate', () => {
|
||||
expect(logger.resolveAction('GET', '/api/v1/auth/gate/plex')).toBe('auth.credential-injection');
|
||||
expect(logger.resolveAction('GET', '/api/v1/auth/gate/jellyfin')).toBe('auth.credential-injection');
|
||||
});
|
||||
|
||||
test('app-token-issue resolves for /auth/app-token', () => {
|
||||
expect(logger.resolveAction('GET', '/api/v1/auth/app-token/plex')).toBe('auth.app-token-issue');
|
||||
expect(logger.resolveAction('GET', '/api/v1/auth/app-token/jellyfin')).toBe('auth.app-token-issue');
|
||||
});
|
||||
|
||||
test('api-key-generate / revoke / jwt-mint resolve', () => {
|
||||
expect(logger.resolveAction('POST', '/api/v1/auth/keys')).toBe('auth.api-key-generate');
|
||||
expect(logger.resolveAction('DELETE', '/api/v1/auth/keys/abc-123')).toBe('auth.api-key-revoke');
|
||||
expect(logger.resolveAction('POST', '/api/v1/auth/jwt')).toBe('auth.jwt-mint');
|
||||
});
|
||||
|
||||
test('existing actions still resolve', () => {
|
||||
expect(logger.resolveAction('POST', '/api/v1/site')).toBe('caddy.add-site');
|
||||
expect(logger.resolveAction('POST', '/api/v1/totp/setup')).toBe('auth.totp-setup');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user