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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user