# A02: Security Misconfiguration Last updated: March 1, 2026 ## Findings addressed 1. SMTP transport debug logging enabled in all environments. 2. Production CORS had a fail-open branch when configured origins were empty. 3. Missing explicit anti-framing headers at edge. 4. Docker compose exposed Postgres/API on all host interfaces. ## Fixes implemented 1. SMTP transport hardening in API: - `requireTLS` now respects config (`SMTP_REQUIRE_TLS`). - SMTP debug/logger are disabled in production (`!isProd` only). 2. CORS production behavior tightened: - Removed fail-open branch for empty configured origins. - Production now allows only explicitly configured origins. 3. Edge header hardening: - Added `X-Frame-Options: DENY`. - Added `Content-Security-Policy: frame-ancestors 'none'`. 4. Compose exposure reduction: - Bound Postgres and API ports to localhost only. ## Files changed 1. `api/src/server.ts` 2. `Caddyfile.prod` 3. `Caddyfile.dev` 4. `deploy/nginx/skymoneybudget.com.conf` 5. `docker-compose.yml` ## Verification ### Automated security regression tests Command (A01 regression): ```bash cd api npm test -- auth.routes.test.ts access-control.account-delete.test.ts ``` Verified output (provided from host run): - Test Files: `2 passed (2)` - Tests: `6 passed (6)` - Start time: `16:39:35` Command (A02 dedicated suite): ```bash cd api npx vitest --run -c vitest.security.config.ts ``` Verified output: - Test Files: `1 passed (1)` - Tests: `5 passed (5)` - Suite: `tests/security-misconfiguration.test.ts` Coverage in dedicated suite: 1. Production CORS allowlist enforcement (allowed origin accepted, denied origin does not receive allow headers). 2. SMTP production mailer options disable debug/logger. 3. Runtime CORS preflight headers validated for allowed origins (`allow-origin`, `allow-credentials`, `vary`). 4. Edge config files contain anti-framing headers (`X-Frame-Options`, `frame-ancestors` CSP). 5. `docker-compose.yml` binds Postgres/API ports to localhost only. ### Expected operational checks after deploy 1. Unauthenticated `GET /dashboard` returns `401`. 2. Spoofed `x-user-id` does not bypass auth when `AUTH_DISABLED=false`. 3. `/admin/rollover` remains inaccessible from public network. 4. Response headers include anti-framing policy. ## Residual notes 1. Keep production env pinned to: - `AUTH_DISABLED=false` - `ALLOW_INSECURE_AUTH_FOR_DEV=false` 2. Keep CORS origins explicitly configured in production: - `CORS_ORIGIN` and/or `CORS_ORIGINS`.