Update security headers in Apache configuration to enhance protection
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s
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.
This commit is contained in:
@@ -21,13 +21,18 @@
|
|||||||
|
|
||||||
# Security Headers
|
# Security Headers
|
||||||
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
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
|
# X-Content-Type-Options wird vom Nuxt-Server gesetzt
|
||||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
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)
|
# 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'; 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'"
|
# 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)
|
# Proxy alle Anfragen an Nuxt Server (Port 3100)
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
|
|||||||
@@ -23,12 +23,16 @@
|
|||||||
|
|
||||||
# Security Headers
|
# Security Headers
|
||||||
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
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 X-Content-Type-Options nosniff
|
||||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
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)
|
# 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'; 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'"
|
# 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
|
# SPA Fallback für Nuxt.js
|
||||||
<Directory "/var/www/harheimertc/dist">
|
<Directory "/var/www/harheimertc/dist">
|
||||||
|
|||||||
@@ -28,15 +28,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
// Merge members: combine manual + registered, detect duplicates
|
// Merge members: combine manual + registered, detect duplicates
|
||||||
const mergedMembers = []
|
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
|
// First, add all manual members and build lookup maps
|
||||||
for (const member of manualMembers) {
|
for (let i = 0; i < manualMembers.length; i++) {
|
||||||
|
const member = manualMembers[i]
|
||||||
const normalizedEmail = member.email?.toLowerCase().trim() || ''
|
const normalizedEmail = member.email?.toLowerCase().trim() || ''
|
||||||
const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim()
|
const fullName = `${member.firstName || ''} ${member.lastName || ''}`.trim()
|
||||||
const normalizedName = fullName.toLowerCase()
|
const normalizedName = fullName.toLowerCase()
|
||||||
|
|
||||||
|
const memberIndex = mergedMembers.length
|
||||||
mergedMembers.push({
|
mergedMembers.push({
|
||||||
...member,
|
...member,
|
||||||
name: fullName, // Computed for display
|
name: fullName, // Computed for display
|
||||||
@@ -45,8 +49,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
hasLogin: false
|
hasLogin: false
|
||||||
})
|
})
|
||||||
|
|
||||||
if (normalizedEmail) processedEmails.add(normalizedEmail)
|
// Build lookup maps (only for manual members)
|
||||||
if (normalizedName) processedNames.add(normalizedName)
|
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)
|
// Then add registered users (only active ones)
|
||||||
@@ -56,21 +71,35 @@ export default defineEventHandler(async (event) => {
|
|||||||
const normalizedEmail = user.email?.toLowerCase().trim() || ''
|
const normalizedEmail = user.email?.toLowerCase().trim() || ''
|
||||||
const normalizedName = user.name?.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
|
let matchedManualIndex = -1
|
||||||
|
|
||||||
// Try to match by email first
|
// Try to match by email first (O(1) lookup)
|
||||||
if (normalizedEmail) {
|
if (normalizedEmail && emailToIndexMap.has(normalizedEmail)) {
|
||||||
matchedManualIndex = mergedMembers.findIndex(
|
matchedManualIndex = emailToIndexMap.get(normalizedEmail)
|
||||||
m => m.source === 'manual' && m.email?.toLowerCase().trim() === normalizedEmail
|
// Verify it's still a manual member (safety check)
|
||||||
)
|
if (mergedMembers[matchedManualIndex]?.source !== 'manual') {
|
||||||
|
matchedManualIndex = -1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no email match, try name
|
// If no email match, try name (O(1) lookup)
|
||||||
if (matchedManualIndex === -1 && normalizedName) {
|
if (matchedManualIndex === -1 && normalizedName && nameToIndexMap.has(normalizedName)) {
|
||||||
matchedManualIndex = mergedMembers.findIndex(
|
matchedManualIndex = nameToIndexMap.get(normalizedName)
|
||||||
m => m.source === 'manual' && m.name?.toLowerCase().trim() === 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) {
|
if (matchedManualIndex !== -1) {
|
||||||
|
|||||||
@@ -17,13 +17,17 @@ export default defineEventHandler((event) => {
|
|||||||
setHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin')
|
setHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin')
|
||||||
setHeader(event, 'Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
|
setHeader(event, 'Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
|
||||||
|
|
||||||
// X-Frame-Options: SAMEORIGIN (DENY wäre strenger, verhindert aber iFrames komplett)
|
// X-Frame-Options entfernt - verwenden CSP frame-ancestors stattdessen
|
||||||
setHeader(event, 'X-Frame-Options', 'SAMEORIGIN')
|
// CSP frame-ancestors ist moderner und unterstützt mehrere Domains
|
||||||
|
|
||||||
// Legacy-Header (optional; moderne Browser verlassen sich primär auf CSP)
|
// Legacy-Header (optional; moderne Browser verlassen sich primär auf CSP)
|
||||||
setHeader(event, 'X-XSS-Protection', '0')
|
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'
|
const cspEnabled = (process.env.CSP_ENABLED || '').toLowerCase() === 'true'
|
||||||
if (cspEnabled) {
|
if (cspEnabled) {
|
||||||
const reportOnly = (process.env.CSP_REPORT_ONLY || 'true').toLowerCase() !== 'false'
|
const reportOnly = (process.env.CSP_REPORT_ONLY || 'true').toLowerCase() !== 'false'
|
||||||
@@ -33,7 +37,7 @@ export default defineEventHandler((event) => {
|
|||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
"frame-ancestors 'self'",
|
`frame-ancestors ${allowedFrameAncestors}`,
|
||||||
// Nuxt lädt Fonts ggf. von Google (siehe nuxt.config.js)
|
// Nuxt lädt Fonts ggf. von Google (siehe nuxt.config.js)
|
||||||
"font-src 'self' https://fonts.gstatic.com data:",
|
"font-src 'self' https://fonts.gstatic.com data:",
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||||
@@ -44,6 +48,9 @@ export default defineEventHandler((event) => {
|
|||||||
].join('; ')
|
].join('; ')
|
||||||
|
|
||||||
setHeader(event, reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy', cspValue)
|
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}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user