mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 19:44:12 +00:00
Tests: - Update frontend.e2e.spec.ts with locale testing - Add search.e2e.spec.ts for search functionality - Add i18n.int.spec.ts for localization tests - Add search.int.spec.ts for search integration - Update playwright.config.ts Documentation: - Add CLAUDE.md with project instructions - Add docs/ directory with detailed documentation - Add scripts/ for utility scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
733 lines
No EOL
21 KiB
Markdown
733 lines
No EOL
21 KiB
Markdown
Kontext
|
|
Du arbeitest auf dem Server sv-payload im Verzeichnis /home/payload/payload-cms.
|
|
Ein Sicherheits-Audit hat kritische Schwachstellen identifiziert, die sofort behoben werden müssen.
|
|
Audit-Findings (KRITISCH)
|
|
#SchwachstelleDateiRisiko1PAYLOAD_SECRET Fallback auf leeren Stringpayload.config.tsToken-Fälschung2CONSENT_LOGGING_API_KEY undefined = BypassConsentLogs.tsUnautorisierter Schreibzugriff3IP_ANONYMIZATION_PEPPER hardcoded FallbackConsentLogs.tsRainbow-Table Angriff4GraphQL Playground in Productiongraphql-playground/route.tsSchema-Leak5Multi-Tenant Read Access ohne Domain-CheckCookieConfigurations.ts, CookieInventory.tsTenant-Daten-Leak
|
|
|
|
Aufgabe 1: Zentrale Environment-Validierung erstellen
|
|
Erstelle src/lib/envValidation.ts:
|
|
typescript// src/lib/envValidation.ts
|
|
|
|
/**
|
|
* Zentrale Validierung aller erforderlichen Environment-Variablen.
|
|
* Wird beim Server-Start aufgerufen und beendet den Prozess bei fehlenden Werten.
|
|
*/
|
|
|
|
interface RequiredEnvVars {
|
|
PAYLOAD_SECRET: string
|
|
DATABASE_URI: string
|
|
CONSENT_LOGGING_API_KEY: string
|
|
IP_ANONYMIZATION_PEPPER: string
|
|
}
|
|
|
|
const FORBIDDEN_VALUES = [
|
|
'',
|
|
'default-pepper-change-me',
|
|
'change-me',
|
|
'your-secret-here',
|
|
'xxx',
|
|
]
|
|
|
|
function validateEnvVar(name: string, value: string | undefined): string {
|
|
if (!value || value.trim() === '') {
|
|
throw new Error(
|
|
`FATAL: Environment variable ${name} is required but not set. ` +
|
|
`Server cannot start without this value.`
|
|
)
|
|
}
|
|
|
|
if (FORBIDDEN_VALUES.includes(value.trim().toLowerCase())) {
|
|
throw new Error(
|
|
`FATAL: Environment variable ${name} has an insecure default value. ` +
|
|
`Please set a secure random value.`
|
|
)
|
|
}
|
|
|
|
return value.trim()
|
|
}
|
|
|
|
/**
|
|
* Validiert alle erforderlichen Environment-Variablen.
|
|
* Wirft einen Fehler und beendet den Server-Start, wenn Variablen fehlen.
|
|
*/
|
|
export function validateRequiredEnvVars(): RequiredEnvVars {
|
|
return {
|
|
PAYLOAD_SECRET: validateEnvVar('PAYLOAD_SECRET', process.env.PAYLOAD_SECRET),
|
|
DATABASE_URI: validateEnvVar('DATABASE_URI', process.env.DATABASE_URI),
|
|
CONSENT_LOGGING_API_KEY: validateEnvVar('CONSENT_LOGGING_API_KEY', process.env.CONSENT_LOGGING_API_KEY),
|
|
IP_ANONYMIZATION_PEPPER: validateEnvVar('IP_ANONYMIZATION_PEPPER', process.env.IP_ANONYMIZATION_PEPPER),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bereits validierte Environment-Variablen.
|
|
* Wird beim Import ausgeführt (Fail-Fast Prinzip).
|
|
*/
|
|
export const env = validateRequiredEnvVars()
|
|
|
|
Aufgabe 2: Tenant-Access Utility erstellen
|
|
Erstelle src/lib/tenantAccess.ts:
|
|
typescript// src/lib/tenantAccess.ts
|
|
|
|
import type { Access, PayloadRequest } from 'payload'
|
|
|
|
/**
|
|
* Ermittelt die Tenant-ID aus dem Request-Host.
|
|
* Gleicht die Domain mit der tenants-Collection ab.
|
|
*/
|
|
export async function getTenantIdFromHost(req: PayloadRequest): Promise<number | null> {
|
|
try {
|
|
// Host-Header extrahieren (unterstützt verschiedene Formate)
|
|
const host =
|
|
req.headers?.host ||
|
|
(req.headers?.get && req.headers.get('host')) ||
|
|
null
|
|
|
|
if (!host || typeof host !== 'string') {
|
|
return null
|
|
}
|
|
|
|
// Domain normalisieren: Port und www entfernen
|
|
const domain = host
|
|
.split(':')[0]
|
|
.replace(/^www\./, '')
|
|
.toLowerCase()
|
|
.trim()
|
|
|
|
if (!domain) {
|
|
return null
|
|
}
|
|
|
|
// Tenant aus Datenbank suchen
|
|
const result = await req.payload.find({
|
|
collection: 'tenants',
|
|
where: {
|
|
domain: { equals: domain }
|
|
},
|
|
limit: 1,
|
|
depth: 0,
|
|
})
|
|
|
|
if (result.docs.length > 0 && result.docs[0]?.id) {
|
|
return Number(result.docs[0].id)
|
|
}
|
|
|
|
return null
|
|
} catch (error) {
|
|
console.error('[TenantAccess] Error resolving tenant from host:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access-Control für öffentlich lesbare, aber tenant-isolierte Collections.
|
|
*
|
|
* - Authentifizierte Admin-User: Voller Lesezugriff
|
|
* - Anonyme Requests: Nur Daten des eigenen Tenants (basierend auf Domain)
|
|
*/
|
|
export const tenantScopedPublicRead: Access = async ({ req }) => {
|
|
// Authentifizierte Admins dürfen alles lesen
|
|
if (req.user) {
|
|
return true
|
|
}
|
|
|
|
// Anonyme Requests: Tenant aus Domain ermitteln
|
|
const tenantId = await getTenantIdFromHost(req)
|
|
|
|
if (!tenantId) {
|
|
// Keine gültige Domain → kein Zugriff
|
|
return false
|
|
}
|
|
|
|
// Nur Dokumente des eigenen Tenants zurückgeben
|
|
return {
|
|
tenant: {
|
|
equals: tenantId
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access-Control: Nur authentifizierte User
|
|
*/
|
|
export const authenticatedOnly: Access = ({ req }) => {
|
|
return !!req.user
|
|
}
|
|
|
|
Aufgabe 3: payload.config.ts aktualisieren
|
|
Aktualisiere src/payload.config.ts:
|
|
Am Anfang der Datei (nach den Imports) hinzufügen:
|
|
typescript// Security: Validate required environment variables at startup
|
|
import { env } from './lib/envValidation'
|
|
Dann in buildConfig ändern:
|
|
typescript// VORHER:
|
|
secret: process.env.PAYLOAD_SECRET || '',
|
|
|
|
// NACHHER:
|
|
secret: env.PAYLOAD_SECRET,
|
|
Und:
|
|
typescript// VORHER:
|
|
db: postgresAdapter({
|
|
pool: {
|
|
connectionString: process.env.DATABASE_URI || '',
|
|
},
|
|
}),
|
|
|
|
// NACHHER:
|
|
db: postgresAdapter({
|
|
pool: {
|
|
connectionString: env.DATABASE_URI,
|
|
},
|
|
}),
|
|
|
|
Aufgabe 4: ConsentLogs.ts komplett ersetzen
|
|
Ersetze src/collections/ConsentLogs.ts mit dieser sicheren Version:
|
|
typescript// src/collections/ConsentLogs.ts
|
|
|
|
import type { CollectionConfig } from 'payload'
|
|
import crypto from 'crypto'
|
|
import { env } from '../lib/envValidation'
|
|
import { authenticatedOnly } from '../lib/tenantAccess'
|
|
|
|
/**
|
|
* Generiert einen täglichen, tenant-spezifischen Salt für IP-Anonymisierung.
|
|
* Verwendet den sicher validierten Pepper aus der Umgebung.
|
|
*/
|
|
function getDailySalt(tenantId: string): string {
|
|
const date = new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(`${env.IP_ANONYMIZATION_PEPPER}-${tenantId}-${date}`)
|
|
.digest('hex')
|
|
}
|
|
|
|
/**
|
|
* Anonymisiert eine IP-Adresse mit HMAC-SHA256.
|
|
* Der Salt rotiert täglich und ist tenant-spezifisch.
|
|
*/
|
|
function anonymizeIp(ip: string, tenantId: string): string {
|
|
const salt = getDailySalt(tenantId)
|
|
return crypto
|
|
.createHmac('sha256', salt)
|
|
.update(ip)
|
|
.digest('hex')
|
|
.substring(0, 32) // Gekürzt für Lesbarkeit
|
|
}
|
|
|
|
/**
|
|
* Extrahiert die Client-IP aus dem Request.
|
|
* Berücksichtigt Reverse-Proxy-Header.
|
|
*/
|
|
function extractClientIp(req: any): string {
|
|
// X-Forwarded-For kann mehrere IPs enthalten (Client, Proxies)
|
|
const forwarded = req.headers?.['x-forwarded-for']
|
|
if (typeof forwarded === 'string') {
|
|
return forwarded.split(',')[0].trim()
|
|
}
|
|
if (Array.isArray(forwarded) && forwarded.length > 0) {
|
|
return String(forwarded[0]).trim()
|
|
}
|
|
|
|
// X-Real-IP (einzelne IP)
|
|
const realIp = req.headers?.['x-real-ip']
|
|
if (typeof realIp === 'string') {
|
|
return realIp.trim()
|
|
}
|
|
|
|
// Fallback: Socket Remote Address
|
|
return req.socket?.remoteAddress || req.ip || 'unknown'
|
|
}
|
|
|
|
/**
|
|
* ConsentLogs Collection - WORM Audit Trail
|
|
*
|
|
* Implementiert das Write-Once-Read-Many Prinzip für DSGVO-Nachweispflicht.
|
|
* Updates und Deletes sind auf API-Ebene deaktiviert.
|
|
*/
|
|
export const ConsentLogs: CollectionConfig = {
|
|
slug: 'consent-logs',
|
|
admin: {
|
|
useAsTitle: 'consentId',
|
|
group: 'Consent Management',
|
|
description: 'WORM Audit-Trail für Cookie-Einwilligungen (unveränderbar)',
|
|
defaultColumns: ['consentId', 'tenant', 'categories', 'revision', 'createdAt'],
|
|
},
|
|
|
|
// Performance: Keine Versionierung für Audit-Logs
|
|
versions: false,
|
|
|
|
access: {
|
|
/**
|
|
* CREATE: Nur mit gültigem API-Key.
|
|
* Beide Seiten (Header UND Env-Variable) müssen existieren und übereinstimmen.
|
|
*/
|
|
create: ({ req }) => {
|
|
const apiKey = req.headers?.['x-api-key']
|
|
|
|
// Strikte Validierung: Header muss existieren und non-empty sein
|
|
if (!apiKey || typeof apiKey !== 'string') {
|
|
return false
|
|
}
|
|
|
|
const trimmedKey = apiKey.trim()
|
|
if (trimmedKey === '') {
|
|
return false
|
|
}
|
|
|
|
// Vergleich mit validiertem Environment-Wert
|
|
// (env.CONSENT_LOGGING_API_KEY ist garantiert non-empty durch envValidation)
|
|
return trimmedKey === env.CONSENT_LOGGING_API_KEY
|
|
},
|
|
|
|
/**
|
|
* READ: Nur authentifizierte Admin-User
|
|
*/
|
|
read: authenticatedOnly,
|
|
|
|
/**
|
|
* UPDATE: WORM - Niemals erlaubt
|
|
*/
|
|
update: () => false,
|
|
|
|
/**
|
|
* DELETE: WORM - Niemals über API erlaubt
|
|
* (Nur via Retention-Job mit direktem DB-Zugriff)
|
|
*/
|
|
delete: () => false,
|
|
},
|
|
|
|
hooks: {
|
|
beforeChange: [
|
|
({ data, req, operation }) => {
|
|
// Nur bei Neuanlage
|
|
if (operation !== 'create') {
|
|
return data
|
|
}
|
|
|
|
// 1. Server-generierte Consent-ID (Trust Boundary)
|
|
data.consentId = crypto.randomUUID()
|
|
|
|
// 2. IP anonymisieren
|
|
const rawIp = data.ip || extractClientIp(req)
|
|
const tenantId = typeof data.tenant === 'object'
|
|
? String(data.tenant.id)
|
|
: String(data.tenant)
|
|
|
|
data.anonymizedIp = anonymizeIp(rawIp, tenantId)
|
|
|
|
// Rohe IP NIEMALS speichern
|
|
delete data.ip
|
|
|
|
// 3. Ablaufdatum setzen (3 Jahre Retention gemäß DSGVO)
|
|
const expiresAt = new Date()
|
|
expiresAt.setFullYear(expiresAt.getFullYear() + 3)
|
|
data.expiresAt = expiresAt.toISOString()
|
|
|
|
// 4. User Agent kürzen (Datensparsamkeit)
|
|
if (data.userAgent && typeof data.userAgent === 'string') {
|
|
data.userAgent = data.userAgent.substring(0, 500)
|
|
}
|
|
|
|
return data
|
|
},
|
|
],
|
|
},
|
|
|
|
fields: [
|
|
{
|
|
name: 'consentId',
|
|
type: 'text',
|
|
required: true,
|
|
unique: true,
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'Server-generierte eindeutige ID',
|
|
},
|
|
},
|
|
{
|
|
name: 'clientRef',
|
|
type: 'text',
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'Client-seitige Referenz (Cookie-UUID) für Traceability',
|
|
},
|
|
},
|
|
{
|
|
name: 'tenant',
|
|
type: 'relationship',
|
|
relationTo: 'tenants',
|
|
required: true,
|
|
admin: {
|
|
readOnly: true,
|
|
},
|
|
},
|
|
{
|
|
name: 'categories',
|
|
type: 'json',
|
|
required: true,
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'Akzeptierte Kategorien zum Zeitpunkt der Einwilligung',
|
|
},
|
|
},
|
|
{
|
|
name: 'revision',
|
|
type: 'number',
|
|
required: true,
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'Version der Konfiguration zum Zeitpunkt der Zustimmung',
|
|
},
|
|
},
|
|
{
|
|
name: 'userAgent',
|
|
type: 'text',
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'Browser/Device (für Forensik und Bot-Erkennung)',
|
|
},
|
|
},
|
|
{
|
|
name: 'anonymizedIp',
|
|
type: 'text',
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'HMAC-Hash der IP (täglich rotierender, tenant-spezifischer Salt)',
|
|
},
|
|
},
|
|
{
|
|
name: 'expiresAt',
|
|
type: 'date',
|
|
required: true,
|
|
admin: {
|
|
readOnly: true,
|
|
description: 'Automatische Löschung nach 3 Jahren',
|
|
date: {
|
|
pickerAppearance: 'dayOnly',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
Aufgabe 5: CookieConfigurations.ts aktualisieren
|
|
Ersetze src/collections/CookieConfigurations.ts:
|
|
typescript// src/collections/CookieConfigurations.ts
|
|
|
|
import type { CollectionConfig } from 'payload'
|
|
import { tenantScopedPublicRead, authenticatedOnly } from '../lib/tenantAccess'
|
|
|
|
/**
|
|
* CookieConfigurations Collection
|
|
*
|
|
* Mandantenspezifische Cookie-Banner-Konfiguration.
|
|
* Öffentlich lesbar, aber nur für den eigenen Tenant (Domain-basiert).
|
|
*/
|
|
export const CookieConfigurations: CollectionConfig = {
|
|
slug: 'cookie-configurations',
|
|
admin: {
|
|
useAsTitle: 'title',
|
|
group: 'Consent Management',
|
|
description: 'Cookie-Banner Konfiguration pro Tenant',
|
|
},
|
|
access: {
|
|
// Öffentlich, aber tenant-isoliert (Domain-Check)
|
|
read: tenantScopedPublicRead,
|
|
create: authenticatedOnly,
|
|
update: authenticatedOnly,
|
|
delete: authenticatedOnly,
|
|
},
|
|
fields: [
|
|
{
|
|
name: 'tenant',
|
|
type: 'relationship',
|
|
relationTo: 'tenants',
|
|
required: true,
|
|
unique: true,
|
|
admin: {
|
|
description: 'Jeder Tenant kann nur eine Konfiguration haben',
|
|
},
|
|
},
|
|
{
|
|
name: 'title',
|
|
type: 'text',
|
|
required: true,
|
|
defaultValue: 'Cookie-Einstellungen',
|
|
admin: {
|
|
description: 'Interner Titel zur Identifikation',
|
|
},
|
|
},
|
|
{
|
|
name: 'revision',
|
|
type: 'number',
|
|
required: true,
|
|
defaultValue: 1,
|
|
admin: {
|
|
description: 'Bei inhaltlichen Änderungen erhöhen → erzwingt erneuten Consent bei allen Nutzern',
|
|
},
|
|
},
|
|
{
|
|
name: 'enabledCategories',
|
|
type: 'select',
|
|
hasMany: true,
|
|
required: true,
|
|
defaultValue: ['necessary', 'analytics'],
|
|
options: [
|
|
{ label: 'Notwendig', value: 'necessary' },
|
|
{ label: 'Funktional', value: 'functional' },
|
|
{ label: 'Statistik', value: 'analytics' },
|
|
{ label: 'Marketing', value: 'marketing' },
|
|
],
|
|
admin: {
|
|
description: 'Welche Kategorien sollen im Banner angezeigt werden?',
|
|
},
|
|
},
|
|
{
|
|
name: 'translations',
|
|
type: 'group',
|
|
fields: [
|
|
{
|
|
name: 'de',
|
|
type: 'group',
|
|
label: 'Deutsch',
|
|
fields: [
|
|
{
|
|
name: 'bannerTitle',
|
|
type: 'text',
|
|
defaultValue: 'Wir respektieren Ihre Privatsphäre',
|
|
},
|
|
{
|
|
name: 'bannerDescription',
|
|
type: 'textarea',
|
|
defaultValue: 'Diese Website verwendet Cookies, um Ihnen die bestmögliche Erfahrung zu bieten.',
|
|
},
|
|
{
|
|
name: 'acceptAllButton',
|
|
type: 'text',
|
|
defaultValue: 'Alle akzeptieren',
|
|
},
|
|
{
|
|
name: 'acceptNecessaryButton',
|
|
type: 'text',
|
|
defaultValue: 'Nur notwendige',
|
|
},
|
|
{
|
|
name: 'settingsButton',
|
|
type: 'text',
|
|
defaultValue: 'Einstellungen',
|
|
},
|
|
{
|
|
name: 'saveButton',
|
|
type: 'text',
|
|
defaultValue: 'Auswahl speichern',
|
|
},
|
|
{
|
|
name: 'privacyPolicyUrl',
|
|
type: 'text',
|
|
defaultValue: '/datenschutz',
|
|
},
|
|
{
|
|
name: 'categoryLabels',
|
|
type: 'group',
|
|
fields: [
|
|
{
|
|
name: 'necessary',
|
|
type: 'group',
|
|
fields: [
|
|
{ name: 'title', type: 'text', defaultValue: 'Notwendig' },
|
|
{ name: 'description', type: 'textarea', defaultValue: 'Diese Cookies sind für die Grundfunktionen der Website erforderlich.' },
|
|
],
|
|
},
|
|
{
|
|
name: 'functional',
|
|
type: 'group',
|
|
fields: [
|
|
{ name: 'title', type: 'text', defaultValue: 'Funktional' },
|
|
{ name: 'description', type: 'textarea', defaultValue: 'Diese Cookies ermöglichen erweiterte Funktionen.' },
|
|
],
|
|
},
|
|
{
|
|
name: 'analytics',
|
|
type: 'group',
|
|
fields: [
|
|
{ name: 'title', type: 'text', defaultValue: 'Statistik' },
|
|
{ name: 'description', type: 'textarea', defaultValue: 'Diese Cookies helfen uns zu verstehen, wie Besucher die Website nutzen.' },
|
|
],
|
|
},
|
|
{
|
|
name: 'marketing',
|
|
type: 'group',
|
|
fields: [
|
|
{ name: 'title', type: 'text', defaultValue: 'Marketing' },
|
|
{ name: 'description', type: 'textarea', defaultValue: 'Diese Cookies werden für Werbezwecke verwendet.' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'styling',
|
|
type: 'group',
|
|
fields: [
|
|
{
|
|
name: 'position',
|
|
type: 'select',
|
|
defaultValue: 'bottom',
|
|
options: [
|
|
{ label: 'Unten', value: 'bottom' },
|
|
{ label: 'Oben', value: 'top' },
|
|
{ label: 'Mitte (Modal)', value: 'middle' },
|
|
],
|
|
},
|
|
{
|
|
name: 'theme',
|
|
type: 'select',
|
|
defaultValue: 'dark',
|
|
options: [
|
|
{ label: 'Dunkel', value: 'dark' },
|
|
{ label: 'Hell', value: 'light' },
|
|
{ label: 'Auto (System)', value: 'auto' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
Aufgabe 6: CookieInventory.ts aktualisieren
|
|
Ersetze src/collections/CookieInventory.ts:
|
|
typescript// src/collections/CookieInventory.ts
|
|
|
|
import type { CollectionConfig } from 'payload'
|
|
import { tenantScopedPublicRead, authenticatedOnly } from '../lib/tenantAccess'
|
|
|
|
/**
|
|
* CookieInventory Collection
|
|
*
|
|
* Dokumentation aller verwendeten Cookies für die Datenschutzerklärung.
|
|
* Öffentlich lesbar, aber nur für den eigenen Tenant (Domain-basiert).
|
|
*/
|
|
export const CookieInventory: CollectionConfig = {
|
|
slug: 'cookie-inventory',
|
|
admin: {
|
|
useAsTitle: 'name',
|
|
group: 'Consent Management',
|
|
description: 'Cookie-Dokumentation für die Datenschutzerklärung',
|
|
defaultColumns: ['name', 'provider', 'category', 'duration', 'tenant'],
|
|
},
|
|
access: {
|
|
// Öffentlich, aber tenant-isoliert (Domain-Check)
|
|
read: tenantScopedPublicRead,
|
|
create: authenticatedOnly,
|
|
update: authenticatedOnly,
|
|
delete: authenticatedOnly,
|
|
},
|
|
fields: [
|
|
{
|
|
name: 'tenant',
|
|
type: 'relationship',
|
|
relationTo: 'tenants',
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'name',
|
|
type: 'text',
|
|
required: true,
|
|
admin: {
|
|
description: 'Technischer Name des Cookies (z.B. "_ga")',
|
|
},
|
|
},
|
|
{
|
|
name: 'provider',
|
|
type: 'text',
|
|
required: true,
|
|
admin: {
|
|
description: 'Anbieter (z.B. "Google LLC")',
|
|
},
|
|
},
|
|
{
|
|
name: 'category',
|
|
type: 'select',
|
|
required: true,
|
|
options: [
|
|
{ label: 'Notwendig', value: 'necessary' },
|
|
{ label: 'Funktional', value: 'functional' },
|
|
{ label: 'Statistik', value: 'analytics' },
|
|
{ label: 'Marketing', value: 'marketing' },
|
|
],
|
|
},
|
|
{
|
|
name: 'duration',
|
|
type: 'text',
|
|
required: true,
|
|
admin: {
|
|
description: 'Speicherdauer (z.B. "2 Jahre")',
|
|
},
|
|
},
|
|
{
|
|
name: 'description',
|
|
type: 'textarea',
|
|
required: true,
|
|
admin: {
|
|
description: 'Verständliche Erklärung für Endnutzer',
|
|
},
|
|
},
|
|
{
|
|
name: 'isActive',
|
|
type: 'checkbox',
|
|
defaultValue: true,
|
|
},
|
|
],
|
|
}
|
|
|
|
Aufgabe 7: GraphQL Playground entfernen
|
|
Lösche die Datei:
|
|
bashrm -f src/app/\(payload\)/api/graphql-playground/route.ts
|
|
Falls das Verzeichnis danach leer ist:
|
|
bashrmdir src/app/\(payload\)/api/graphql-playground/ 2>/dev/null || true
|
|
|
|
Aufgabe 8: Build und Test
|
|
Nach allen Änderungen:
|
|
bash# TypeScript kompilieren und prüfen
|
|
pnpm build
|
|
|
|
# Bei Erfolg: PM2 neu starten
|
|
pm2 restart payload
|
|
|
|
# Logs prüfen (sollte ohne Fehler starten)
|
|
pm2 logs payload --lines 20 --nostream
|
|
|
|
Aufgabe 9: Sicherheitstest
|
|
Teste die Fixes:
|
|
bash# 1. Tenant-Isolation testen (sollte 403 oder leeres Array zurückgeben)
|
|
curl -s "http://localhost:3000/api/cookie-configurations" | head -c 200
|
|
|
|
# 2. Mit korrektem Host-Header (sollte Daten für porwoll.de zeigen)
|
|
curl -s -H "Host: porwoll.de" "http://localhost:3000/api/cookie-configurations" | head -c 200
|
|
|
|
# 3. Consent-Log ohne API-Key (sollte 403 zurückgeben)
|
|
curl -X POST "http://localhost:3000/api/consent-logs" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"tenant":1,"categories":["necessary"],"revision":1}'
|
|
|
|
# 4. Consent-Log mit korrektem API-Key (sollte 201 zurückgeben)
|
|
curl -X POST "http://localhost:3000/api/consent-logs" \
|
|
-H "Content-Type: application/json" \
|
|
-H "x-api-key: $(grep CONSENT_LOGGING_API_KEY .env | cut -d= -f2)" \
|
|
-d '{"tenant":1,"categories":["necessary"],"revision":1,"clientRef":"test-123"}'
|
|
|
|
# 5. GraphQL Playground (sollte 404 zurückgeben)
|
|
curl -s "http://localhost:3000/api/graphql-playground"
|
|
|
|
Zusammenfassung der Änderungen
|
|
DateiAktionZwecksrc/lib/envValidation.tsNEUFail-Fast für fehlende Env-Varssrc/lib/tenantAccess.tsNEUDomain-basierte Tenant-Isolationsrc/payload.config.tsÄNDERNImport envValidation, sichere Secret-Verwendungsrc/collections/ConsentLogs.tsERSETZENStrikte API-Key-Prüfung, kein Pepper-Fallbacksrc/collections/CookieConfigurations.tsERSETZENTenant-scoped Read Accesssrc/collections/CookieInventory.tsERSETZENTenant-scoped Read Accesssrc/app/(payload)/api/graphql-playground/route.tsLÖSCHENKein Schema-Leak in Production
|
|
Erwartetes Ergebnis
|
|
|
|
Server startet NUR wenn alle Env-Vars gesetzt sind
|
|
Anonyme API-Requests sehen nur Daten ihres Tenants
|
|
ConsentLogs nur mit validem API-Key beschreibbar
|
|
GraphQL Playground nicht mehr erreichbar
|
|
IP-Anonymisierung ohne unsichere Fallbacks |