security: fix 17 vulnerabilities from comprehensive pentest

Fixes identified from full security audit covering auth, crypto,
injection, infrastructure, and configuration security.

Critical:
- C1: Fail-closed on unrecognized NODE_ENV (prevent DEV_SECRET in staging)
- C3: Validate API token expires_at (reject invalid dates that bypass expiry)

High:
- H1: Refresh JWT role from DB on each session (reflect demotions immediately)
- H2: Docker socket proxy for l4-port-manager (restrict API surface)
- H5: Block dangerous WAF custom directives (SecRuleEngine, SecAuditEngine)
- H7: Require explicit NEXTAUTH_TRUST_HOST instead of always trusting Host
- H8: Semantic validation of sync payload (block metadata SSRF, size limits)

Medium:
- M3: Rate limit password change current-password verification
- M5: Parameterized SQL in log/waf parsers (replace template literals)
- M6: Nonce-based CSP replacing unsafe-inline for script-src
- M9: Strip Caddy placeholders from rewrite path_prefix
- M10: Sanitize authentik outpostDomain (path traversal, placeholders)
- M14: Deny access on missing JWT role instead of defaulting to "user"

Low:
- L1: Require Origin header on mutating session-authenticated requests
- L4: Enforce password complexity on user password changes
- L5: Time-limited legacy SHA-256 key fallback (grace period until 2026-06-01)
- L6: Escape LIKE metacharacters in audit log search
- L7: Runtime-validate WAF excluded_rule_ids as positive integers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-03-26 12:14:44 +01:00
parent 7a12ecf2fe
commit debd0d98fc
18 changed files with 339 additions and 77 deletions

View File

@@ -390,19 +390,28 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
// Add user info from token to session
if (session.user && token.id) {
session.user.id = token.id as string;
session.user.role = token.role as string;
session.user.provider = token.provider as string;
// Fetch current avatar from database to ensure it's always up-to-date
// H1: Always fetch current role and avatar from database to reflect
// role changes (e.g. demotion) without waiting for JWT expiry
const userId = Number(token.id);
const currentUser = await getUserById(userId);
session.user.image = currentUser?.avatar_url ?? (token.image as string | null | undefined);
if (currentUser) {
session.user.role = currentUser.role;
session.user.image = currentUser.avatar_url ?? (token.image as string | null | undefined);
} else {
// User deleted from DB — deny access by clearing session
session.user.role = token.role as string;
session.user.image = token.image as string | null | undefined;
}
}
return session;
},
},
secret: config.sessionSecret,
trustHost: true,
// H7: Do not blindly trust Host header — use NEXTAUTH_URL instead.
// trustHost is only safe behind a proxy that normalizes the Host header.
trustHost: !!process.env.NEXTAUTH_TRUST_HOST,
basePath: "/api/auth",
});
@@ -442,7 +451,18 @@ export async function requireAdmin() {
*/
export function checkSameOrigin(request: NextRequest): NextResponse | null {
const origin = request.headers.get("origin");
if (!origin) return null; // same-origin requests may omit Origin
// L1: 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) {
// Allow non-mutating requests without Origin (normal browser behavior)
if (!isMutating) return null;
// For mutating requests, require Origin header
return NextResponse.json({ error: "Forbidden: Origin header required" }, { status: 403 });
}
const host = request.headers.get("host");
try {