From 12ae192b37fc4a247d5db2bd9a8b36e20296dac0 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 11 Jan 2026 20:59:42 +0100 Subject: [PATCH] Update security headers in Apache configuration to enhance protection This commit removes the X-Frame-Options header in favor of using Content Security Policy (CSP) with frame-ancestors for better flexibility and modern security practices. It also adds a fallback for frame-ancestors in case CSP is not enabled. Additionally, the JavaScript middleware is updated to reflect these changes, ensuring consistent security header management across the application. --- apache-ssl-config.conf | 11 +++-- apache-static.conf | 10 +++-- server/api/members.get.js | 63 +++++++++++++++++++-------- server/middleware/security-headers.js | 17 +++++--- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/apache-ssl-config.conf b/apache-ssl-config.conf index 23d4ac6..8958604 100644 --- a/apache-ssl-config.conf +++ b/apache-ssl-config.conf @@ -21,13 +21,18 @@ # Security Headers Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" - Header always set X-Frame-Options SAMEORIGIN + # X-Frame-Options entfernt - verwenden CSP frame-ancestors stattdessen (modernere Lösung) + # Header always set X-Frame-Options SAMEORIGIN # X-Content-Type-Options wird vom Nuxt-Server gesetzt Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()" + + # Frame-Ancestors: Erlaubt Einbettung von harheimertc.de und www.harheimertc.de + # Wird vom Nuxt-Server gesetzt, aber hier als Fallback für den Fall, dass CSP nicht aktiviert ist + Header always set Content-Security-Policy "frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de" - # Optional: Content Security Policy (zuerst Report-Only testen) - # Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'" + # Optional: Vollständige Content Security Policy (zusätzlich zu frame-ancestors) + # Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'" # Proxy alle Anfragen an Nuxt Server (Port 3100) ProxyPreserveHost On diff --git a/apache-static.conf b/apache-static.conf index 61768cf..c2ad571 100644 --- a/apache-static.conf +++ b/apache-static.conf @@ -23,12 +23,16 @@ # Security Headers Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" - Header always set X-Frame-Options SAMEORIGIN + # X-Frame-Options entfernt - verwenden CSP frame-ancestors stattdessen (modernere Lösung) + # Header always set X-Frame-Options SAMEORIGIN Header always set X-Content-Type-Options nosniff Header always set Referrer-Policy "strict-origin-when-cross-origin" + + # Frame-Ancestors: Erlaubt Einbettung von harheimertc.de und www.harheimertc.de + Header always set Content-Security-Policy "frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de" - # Optional: Content Security Policy (zuerst Report-Only testen) - # Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'" + # Optional: Vollständige Content Security Policy (zusätzlich zu frame-ancestors) + # Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'" # SPA Fallback für Nuxt.js diff --git a/server/api/members.get.js b/server/api/members.get.js index 7d78a44..6ce1b67 100644 --- a/server/api/members.get.js +++ b/server/api/members.get.js @@ -28,15 +28,19 @@ export default defineEventHandler(async (event) => { // Merge members: combine manual + registered, detect duplicates const mergedMembers = [] - const processedEmails = new Set() - const processedNames = new Set() + + // Create lookup maps for O(1) matching instead of O(n) findIndex + const emailToIndexMap = new Map() // email -> index in mergedMembers + const nameToIndexMap = new Map() // name -> index in mergedMembers - // First, add all manual members - for (const member of manualMembers) { + // First, add all manual members and build lookup maps + for (let i = 0; i < manualMembers.length; i++) { + const member = manualMembers[i] const normalizedEmail = member.email?.toLowerCase().trim() || '' const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim() const normalizedName = fullName.toLowerCase() + const memberIndex = mergedMembers.length mergedMembers.push({ ...member, name: fullName, // Computed for display @@ -45,8 +49,19 @@ export default defineEventHandler(async (event) => { hasLogin: false }) - if (normalizedEmail) processedEmails.add(normalizedEmail) - if (normalizedName) processedNames.add(normalizedName) + // Build lookup maps (only for manual members) + if (normalizedEmail) { + // Only add if not already present (prefer first occurrence) + if (!emailToIndexMap.has(normalizedEmail)) { + emailToIndexMap.set(normalizedEmail, memberIndex) + } + } + if (normalizedName) { + // Only add if not already present (prefer first occurrence) + if (!nameToIndexMap.has(normalizedName)) { + nameToIndexMap.set(normalizedName, memberIndex) + } + } } // Then add registered users (only active ones) @@ -56,21 +71,35 @@ export default defineEventHandler(async (event) => { const normalizedEmail = user.email?.toLowerCase().trim() || '' const normalizedName = user.name?.toLowerCase().trim() || '' - // Check if this user matches an existing manual member + // Check if this user matches an existing manual member using O(1) lookup let matchedManualIndex = -1 - // Try to match by email first - if (normalizedEmail) { - matchedManualIndex = mergedMembers.findIndex( - m => m.source === 'manual' && m.email?.toLowerCase().trim() === normalizedEmail - ) + // Try to match by email first (O(1) lookup) + if (normalizedEmail && emailToIndexMap.has(normalizedEmail)) { + matchedManualIndex = emailToIndexMap.get(normalizedEmail) + // Verify it's still a manual member (safety check) + if (mergedMembers[matchedManualIndex]?.source !== 'manual') { + matchedManualIndex = -1 + } } - // If no email match, try name - if (matchedManualIndex === -1 && normalizedName) { - matchedManualIndex = mergedMembers.findIndex( - m => m.source === 'manual' && m.name?.toLowerCase().trim() === normalizedName - ) + // If no email match, try name (O(1) lookup) + if (matchedManualIndex === -1 && normalizedName && nameToIndexMap.has(normalizedName)) { + matchedManualIndex = nameToIndexMap.get(normalizedName) + // Verify it's still a manual member and email doesn't conflict (safety check) + const candidate = mergedMembers[matchedManualIndex] + if (candidate?.source === 'manual') { + // Additional safety: if candidate has email, make sure it doesn't conflict + const candidateEmail = candidate.email?.toLowerCase().trim() || '' + if (!candidateEmail || candidateEmail === normalizedEmail) { + // Safe to match by name + } else { + // Email mismatch - don't match by name alone + matchedManualIndex = -1 + } + } else { + matchedManualIndex = -1 + } } if (matchedManualIndex !== -1) { diff --git a/server/middleware/security-headers.js b/server/middleware/security-headers.js index 7e4b9e0..f5728ad 100644 --- a/server/middleware/security-headers.js +++ b/server/middleware/security-headers.js @@ -17,13 +17,17 @@ export default defineEventHandler((event) => { setHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin') setHeader(event, 'Permissions-Policy', 'geolocation=(), microphone=(), camera=()') - // X-Frame-Options: SAMEORIGIN (DENY wäre strenger, verhindert aber iFrames komplett) - setHeader(event, 'X-Frame-Options', 'SAMEORIGIN') - + // X-Frame-Options entfernt - verwenden CSP frame-ancestors stattdessen + // CSP frame-ancestors ist moderner und unterstützt mehrere Domains + // Legacy-Header (optional; moderne Browser verlassen sich primär auf CSP) setHeader(event, 'X-XSS-Protection', '0') - // Optional: CSP + // Frame-Ancestors (für Einbettung von harheimertc.de erlauben) + const allowedFrameAncestors = process.env.FRAME_ANCESTORS || + "'self' https://harheimertc.de https://www.harheimertc.de" + + // Optional: Vollständige CSP const cspEnabled = (process.env.CSP_ENABLED || '').toLowerCase() === 'true' if (cspEnabled) { const reportOnly = (process.env.CSP_REPORT_ONLY || 'true').toLowerCase() !== 'false' @@ -33,7 +37,7 @@ export default defineEventHandler((event) => { "default-src 'self'", "base-uri 'self'", "object-src 'none'", - "frame-ancestors 'self'", + `frame-ancestors ${allowedFrameAncestors}`, // Nuxt lädt Fonts ggf. von Google (siehe nuxt.config.js) "font-src 'self' https://fonts.gstatic.com data:", "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", @@ -44,6 +48,9 @@ export default defineEventHandler((event) => { ].join('; ') setHeader(event, reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy', cspValue) + } else { + // Wenn CSP nicht aktiviert ist, setze nur frame-ancestors + setHeader(event, 'Content-Security-Policy', `frame-ancestors ${allowedFrameAncestors}`) } })