diff --git a/app/api/instances/sync/route.ts b/app/api/instances/sync/route.ts index f17beb24..37a9d6fc 100644 --- a/app/api/instances/sync/route.ts +++ b/app/api/instances/sync/route.ts @@ -202,7 +202,7 @@ function isL4ProxyHost(value: unknown): value is NonNullable): string | null { @@ -341,7 +341,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Invalid sync payload structure" }, { status: 400 }); } - // H8: Semantic validation of proxy host content + // Semantic validation of proxy host content for (const host of (payload as SyncPayload).data.proxyHosts) { const err = validateProxyHostContent(host as unknown as Record); if (err) { diff --git a/app/api/user/change-password/route.ts b/app/api/user/change-password/route.ts index 686594c6..26113f46 100644 --- a/app/api/user/change-password/route.ts +++ b/app/api/user/change-password/route.ts @@ -15,7 +15,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - // M3: Rate limit password change attempts to prevent brute-forcing current password + // Rate limit password change attempts to prevent brute-forcing current password const rateLimitKey = `password-change:${session.user.id}`; const rateCheck = isRateLimited(rateLimitKey); if (rateCheck.blocked) { @@ -28,7 +28,7 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { currentPassword, newPassword } = body; - // L4: Enforce password complexity matching production admin password requirements + // Enforce password complexity matching production admin password requirements if (!newPassword || newPassword.length < 12) { return NextResponse.json( { error: "New password must be at least 12 characters long" }, diff --git a/app/api/v1/tokens/route.ts b/app/api/v1/tokens/route.ts index 69ad38c2..c3a6111f 100644 --- a/app/api/v1/tokens/route.ts +++ b/app/api/v1/tokens/route.ts @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "name is required" }, { status: 400 }); } - // C3: Validate expires_at before passing to createApiToken + // Validate expires_at before passing to createApiToken if (body.expires_at !== undefined && body.expires_at !== null && typeof body.expires_at !== "string") { return NextResponse.json({ error: "expires_at must be a string (ISO 8601 date)" }, { status: 400 }); } diff --git a/docker-compose.yml b/docker-compose.yml index 3f5ec859..d8bcc4f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,9 +111,8 @@ services: retries: 3 start_period: 10s - # H2: Docker socket proxy — restricts API surface exposed to l4-port-manager. + # Docker socket proxy — restricts API surface exposed to l4-port-manager. # Only allows GET, POST to /containers/ and /compose/ endpoints. - # Prevents container escape via unrestricted Docker API access. docker-socket-proxy: container_name: caddy-proxy-manager-docker-proxy image: tecnativa/docker-socket-proxy:latest diff --git a/next.config.mjs b/next.config.mjs index 45b8e93f..0cbd1dfb 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -20,9 +20,8 @@ const nextConfig = { } }, output: 'standalone', - // M6: Security headers (CSP, X-Frame-Options, etc.) are set per-request in - // proxy.ts middleware with a unique nonce, so they are NOT defined here. - // Static headers() would override the nonce-based CSP with a nonce-less one. + // Security headers (CSP, etc.) are set per-request in proxy.ts middleware + // with a unique nonce, so they are NOT defined here as static headers. }; export default nextConfig; diff --git a/proxy.ts b/proxy.ts index 5af95282..ee63a39a 100644 --- a/proxy.ts +++ b/proxy.ts @@ -13,7 +13,7 @@ import crypto from "node:crypto"; const isDev = process.env.NODE_ENV === "development"; /** - * M6: Build a nonce-based Content-Security-Policy per request. + * Build a nonce-based Content-Security-Policy per request. * Next.js reads the nonce from the CSP request header and applies it * to all inline scripts it generates. */ diff --git a/src/lib/api-auth.ts b/src/lib/api-auth.ts index 4c2c87d3..30ee7c91 100644 --- a/src/lib/api-auth.ts +++ b/src/lib/api-auth.ts @@ -46,7 +46,7 @@ export async function authenticateApiRequest( throw new ApiAuthError("Unauthorized", 401); } - // M14: Deny access when role is missing rather than defaulting to "user" + // Deny access when role is missing rather than defaulting to "user" const role = session.user.role; if (!role) { throw new ApiAuthError("Session missing role claim", 401); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8accb131..c8b410bf 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -392,7 +392,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ session.user.id = token.id as string; session.user.provider = token.provider as string; - // H1: Always fetch current role and avatar from database to reflect + // Always fetch current role from database to reflect // role changes (e.g. demotion) without waiting for JWT expiry const userId = Number(token.id); const currentUser = await getUserById(userId); @@ -409,7 +409,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }, }, secret: config.sessionSecret, - // H7: Only trust Host header when explicitly opted in or when NEXTAUTH_URL + // Only trust Host header when explicitly opted in or when NEXTAUTH_URL // is set (operator has declared the canonical URL, so Host validation is moot). trustHost: !!process.env.NEXTAUTH_TRUST_HOST || !!process.env.NEXTAUTH_URL, basePath: "/api/auth", @@ -451,10 +451,8 @@ export async function requireAdmin() { */ export function checkSameOrigin(request: NextRequest): NextResponse | null { const origin = request.headers.get("origin"); - // L1: For mutating requests, require Origin header to be present. + // For mutating requests, require Origin header to be present. // Browsers always send Origin on cross-origin POST/PUT/DELETE. - // A missing Origin on a mutating request from a cookie-authenticated session - // could indicate a non-browser attacker with a stolen cookie. const method = request.method.toUpperCase(); const isMutating = method !== "GET" && method !== "HEAD" && method !== "OPTIONS"; if (!origin) { diff --git a/src/lib/caddy-waf.ts b/src/lib/caddy-waf.ts index 32461729..692a861d 100644 --- a/src/lib/caddy-waf.ts +++ b/src/lib/caddy-waf.ts @@ -108,7 +108,7 @@ export function buildWafHandler(waf: WafSettings, allowWebsocket = false): Recor ); } - // L7: Runtime-validate excluded_rule_ids are positive integers + // Runtime-validate excluded_rule_ids are positive integers if (waf.excluded_rule_ids?.length) { const validIds = waf.excluded_rule_ids.filter( (id): id is number => typeof id === "number" && Number.isFinite(id) && id > 0 && Number.isInteger(id) @@ -132,7 +132,7 @@ export function buildWafHandler(waf: WafSettings, allowWebsocket = false): Recor 'SecResponseBodyAccess Off', ); - // H5: Validate WAF custom directives — block dangerous engine-level overrides + // Block dangerous engine-level overrides in custom directives if (waf.custom_directives?.trim()) { const directives = waf.custom_directives.trim(); const forbiddenPatterns = [ diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 5279af9e..19d70799 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -800,7 +800,7 @@ async function buildProxyRoutes( outpostRoute = { match: [ { - // M10: Sanitize outpostDomain to prevent path traversal and placeholder injection + // Sanitize outpostDomain to prevent path traversal and placeholder injection path: [`/${authentik.outpostDomain.replace(/\.\./g, '').replace(/\{[^}]*\}/g, '').replace(/\/+/g, '/')}/*`] } ], @@ -879,7 +879,7 @@ async function buildProxyRoutes( } // Structured path prefix rewrite - // M9: Sanitize path_prefix to prevent Caddy placeholder injection + // Sanitize path_prefix to prevent Caddy placeholder injection if (meta.rewrite?.path_prefix) { const safePrefix = meta.rewrite.path_prefix.replace(/\{[^}]*\}/g, ''); if (safePrefix) { diff --git a/src/lib/config.ts b/src/lib/config.ts index 1baf1ee7..97d01174 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -30,8 +30,7 @@ function resolveSessionSecret(): string { return DEV_SECRET; } - // C1: Fail-closed on unrecognized NODE_ENV to prevent silent DEV_SECRET usage - // in staging, test, or misconfigured environments. + // Fail-closed on unrecognized NODE_ENV to prevent silent DEV_SECRET usage if (!isDevelopment && !isProduction && !secret) { throw new Error( `SESSION_SECRET is required when NODE_ENV="${process.env.NODE_ENV ?? ""}" ` + diff --git a/src/lib/log-parser.ts b/src/lib/log-parser.ts index 1529bf6d..ed27bdac 100644 --- a/src/lib/log-parser.ts +++ b/src/lib/log-parser.ts @@ -161,7 +161,7 @@ function insertBatch(rows: typeof trafficEvents.$inferInsert[]): void { function purgeOldEntries(): void { const cutoff = Math.floor(Date.now() / 1000) - RETENTION_DAYS * 86400; - // M5: Use parameterized query instead of string interpolation + // Use parameterized query instead of string interpolation db.run(sql`DELETE FROM traffic_events WHERE ts < ${cutoff}`); } diff --git a/src/lib/models/api-tokens.ts b/src/lib/models/api-tokens.ts index 79ceb413..04715aea 100644 --- a/src/lib/models/api-tokens.ts +++ b/src/lib/models/api-tokens.ts @@ -34,7 +34,7 @@ export async function createApiToken( createdBy: number, expiresAt?: string ): Promise<{ token: ApiToken; rawToken: string }> { - // C3: Validate expires_at is a valid ISO 8601 date in the future + // Validate expires_at is a valid ISO 8601 date in the future let validatedExpiresAt: string | null = null; if (expiresAt) { const parsed = new Date(expiresAt); diff --git a/src/lib/models/audit.ts b/src/lib/models/audit.ts index 150099c7..378362e6 100644 --- a/src/lib/models/audit.ts +++ b/src/lib/models/audit.ts @@ -12,7 +12,7 @@ export type AuditEvent = { created_at: string; }; -// L6: Escape LIKE metacharacters so user input is treated as literal text +// Escape LIKE metacharacters so user input is treated as literal text function escapeLikePattern(input: string): string { return input.replace(/[%_\\]/g, (ch) => `\\${ch}`); } diff --git a/src/lib/secret.ts b/src/lib/secret.ts index dd38f5bf..0689c4cc 100644 --- a/src/lib/secret.ts +++ b/src/lib/secret.ts @@ -32,7 +32,7 @@ export function encryptSecret(value: string): string { } /** - * L5: Legacy fallback is time-limited. After the migration grace period, + * Legacy fallback is time-limited. After the migration grace period, * the legacy key is no longer tried, forcing re-encryption of old secrets. * Set LEGACY_KEY_CUTOFF_DATE env var to extend/disable (ISO 8601 date or "never"). */ @@ -49,7 +49,7 @@ export function decryptSecret(value: string): string { try { return _decryptWithKey(value, deriveKey()); } catch (hkdfError: unknown) { - // L5: Only fall back to legacy key within the grace period + // Only fall back to legacy key within the grace period if (LEGACY_KEY_CUTOFF && new Date() > LEGACY_KEY_CUTOFF) { throw new Error( "[secret] HKDF decryption failed and legacy key grace period has expired. " + diff --git a/src/lib/waf-log-parser.ts b/src/lib/waf-log-parser.ts index 94093c12..5e945467 100644 --- a/src/lib/waf-log-parser.ts +++ b/src/lib/waf-log-parser.ts @@ -213,7 +213,7 @@ function insertBatch(rows: typeof wafEvents.$inferInsert[]): void { function purgeOldEntries(): void { const cutoff = Math.floor(Date.now() / 1000) - RETENTION_DAYS * 86400; - // M5: Use parameterized query instead of string interpolation + // Use parameterized query instead of string interpolation db.run(sql`DELETE FROM waf_events WHERE ts < ${cutoff}`); }