chore: remove finding-ID prefixes from code comments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,7 +202,7 @@ function isL4ProxyHost(value: unknown): value is NonNullable<SyncPayload["data"]
|
||||
}
|
||||
|
||||
/**
|
||||
* H8: Validate semantic content of proxy host fields to prevent
|
||||
* 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 {
|
||||
@@ -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<string, unknown>);
|
||||
if (err) {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+3
-5
@@ -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) {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+1
-2
@@ -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 ?? ""}" ` +
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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. " +
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user