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:
@@ -201,6 +201,57 @@ function isL4ProxyHost(value: unknown): value is NonNullable<SyncPayload["data"]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* H8: Validate semantic content of proxy host fields to prevent
|
||||
* config injection via compromised master or stolen sync token.
|
||||
*/
|
||||
function validateProxyHostContent(host: Record<string, unknown>): string | null {
|
||||
// Validate domains are valid hostnames
|
||||
if (typeof host.domains === "string" && host.domains) {
|
||||
try {
|
||||
const domains = JSON.parse(host.domains);
|
||||
if (Array.isArray(domains)) {
|
||||
for (const d of domains) {
|
||||
if (typeof d !== "string" || d.length > 253) {
|
||||
return `Invalid domain in proxy host ${host.id}: ${String(d).slice(0, 50)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// domains might be comma-separated string; just check length
|
||||
if (host.domains.length > 5000) {
|
||||
return `Proxy host ${host.id} domains field too large`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate upstreams don't target dangerous internal services
|
||||
if (typeof host.upstreams === "string" && host.upstreams) {
|
||||
try {
|
||||
const upstreams = JSON.parse(host.upstreams);
|
||||
if (Array.isArray(upstreams)) {
|
||||
for (const u of upstreams) {
|
||||
if (typeof u !== "string") continue;
|
||||
const lower = u.toLowerCase();
|
||||
// Block cloud metadata endpoints
|
||||
if (lower.includes("169.254.169.254") || lower.includes("metadata.google")) {
|
||||
return `Proxy host ${host.id} upstream targets blocked metadata endpoint: ${u.slice(0, 80)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// non-JSON upstreams — skip
|
||||
}
|
||||
}
|
||||
|
||||
// Validate meta field size to prevent oversized config injection
|
||||
if (typeof host.meta === "string" && host.meta && host.meta.length > 100_000) {
|
||||
return `Proxy host ${host.id} meta field exceeds 100KB limit`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the payload has the expected structure for syncing
|
||||
*/
|
||||
@@ -290,6 +341,14 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Invalid sync payload structure" }, { status: 400 });
|
||||
}
|
||||
|
||||
// H8: Semantic validation of proxy host content
|
||||
for (const host of (payload as SyncPayload).data.proxyHosts) {
|
||||
const err = validateProxyHostContent(host as unknown as Record<string, unknown>);
|
||||
if (err) {
|
||||
return NextResponse.json({ error: err }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Backfill l4ProxyHosts for payloads from older master instances that don't include it
|
||||
const normalizedPayload: SyncPayload = {
|
||||
|
||||
Reference in New Issue
Block a user