diff --git a/server/routes/funktionen.js b/server/routes/funktionen.js index d9db219..12da140 100644 --- a/server/routes/funktionen.js +++ b/server/routes/funktionen.js @@ -70,7 +70,15 @@ router.get('/', requireSession, async (req, res) => { await runActivation({ customerId }).catch((err) => console.error('[funktionen] Freischalten fehlgeschlagen:', err.message)) await backfillInvoiceLinks(customerId) const projects = await listCustomerFeatures(customerId) - for (const p of projects) p.analyse = await analyseView(customerId, p.projectId) + // Ein Fehler bei einem Projekt (z. B. Abo-Pruefung) darf nicht die ganze Liste kosten + await Promise.all( + projects.map(async (p) => { + p.analyse = await analyseView(customerId, p.projectId).catch((err) => { + console.error('[funktionen] Analyse-Zustand fehlgeschlagen:', err.message) + return { state: 'unknown' } + }) + }), + ) res.json({ projects, setupEstimate: config.funktionen.setupEstimate, @@ -217,7 +225,7 @@ router.get('/analyse/stats', analyseGuard, async (req, res) => { router.post('/analyse/heatmap-link', analyseGuard, async (req, res) => { try { const { projectId, path, vp } = req.body || {} - if (typeof path !== 'string' || !path.startsWith('/') || path.length > 300 || /[?#]/.test(path)) { + if (typeof path !== 'string' || !path.startsWith('/') || path.length > 300 || /[?#\\\s\u0000-\u001f\u007f]/.test(path)) { return res.status(400).json({ error: 'Ungültiger Pfad' }) } if (!['m', 't', 'd'].includes(vp)) return res.status(400).json({ error: 'Ungültige Ansicht' }) @@ -226,6 +234,7 @@ router.post('/analyse/heatmap-link', analyseGuard, async (req, res) => { return res.status(409).json({ error: 'Die Website-Analyse ist für dieses Projekt nicht aktiv.' }) } const domains = analyseDomains(project) + if (!domains.length) return res.status(409).json({ error: 'Keine gültige Domain' }) const host = project.liveDomain ? domains[domains.length - 1] : domains[0] const token = analyseDeps.client.heatToken({ siteKey: doc.siteKey, path, vp }) res.json({ url: `https://${host}${path}?__wk_heat=${encodeURIComponent(token)}` }) diff --git a/server/services/analyse.js b/server/services/analyse.js index 1d23f9b..9b22f33 100644 --- a/server/services/analyse.js +++ b/server/services/analyse.js @@ -56,7 +56,9 @@ export async function analyseEligibility(customerId, projectId) { return { ok: true, project } } -/** `.project.webklar.com` plus Live-Domain (klein, ohne Schema/Pfad), ohne Doppelte. */ +const HOSTNAME_RE = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/ + +/** `.project.webklar.com` plus Live-Domain (klein, ohne Schema/Pfad), ohne Doppelte; ungueltige Hosts werden uebersprungen. */ export function analyseDomains(project) { const domains = [] if (project.subdomain) domains.push(`${String(project.subdomain).trim().toLowerCase()}.project.webklar.com`) @@ -66,7 +68,7 @@ export function analyseDomains(project) { .replace(/^[a-z]+:\/\//, '') .replace(/[/?#].*$/, '') if (live) domains.push(live) - return [...new Set(domains)] + return [...new Set(domains)].filter((d) => HOSTNAME_RE.test(d)) } async function customerOf(customerId) { diff --git a/test/analyseRoutes.test.js b/test/analyseRoutes.test.js index b6d162a..5b7ca2d 100644 --- a/test/analyseRoutes.test.js +++ b/test/analyseRoutes.test.js @@ -218,3 +218,22 @@ test('Fix b: Abschalten durch Mitarbeiter meldet Sammeldienst active:false', asy an.analyseDeps.client.putSite = real assert.equal(again.state, 'disabled') }) + +test('Härtung: ungültige Hosts, Pfadzeichen, Fehler bei einem Projekt', async () => { + // analyseDomains: ungueltige Hosts werden uebersprungen + assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'https://Cafe.DE/x' }), ['cafe.project.webklar.com', 'cafe.de']) + assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'evil.de@x y' }), ['cafe.project.webklar.com']) + assert.deepEqual(an.analyseDomains({ subdomain: 'ca fe', liveDomain: 'a_b.de' }), []) + assert.deepEqual(an.analyseDomains({ subdomain: 'cafe', liveDomain: 'localhost' }), ['cafe.project.webklar.com']) + // Heatmap-Pfad: Steuerzeichen, Backslash, Leerraum => 400; dekodierte Form bleibt erlaubt + await post(`${A}/activate`, { projectId: 'p1', consent: consent() }, { cookie }) + const link = (path) => post(`${A}/heatmap-link`, { projectId: 'p1', path, vp: 'd' }, { cookie }) + for (const path of ['/a\\b', '/a b', '/a\nb', '/a\u0000b', '/a\tb', '/a\u007fb']) assert.equal((await link(path)).status, 400, JSON.stringify(path)) + assert.equal((await link('/über-uns')).status, 200) + // keine gueltige Domain => 409 + aw.seed('websiteProjects', { $id: 'p7', customerId: 'c1', projectName: 'Kaputt', subdomain: 'ka putt', liveDomain: 'x_y', isPublic: true }) + await post(`${A}/activate`, { projectId: 'p7', consent: consent() }, { cookie }) + const bad = await post(`${A}/heatmap-link`, { projectId: 'p7', path: '/', vp: 'd' }, { cookie }) + assert.equal(bad.status, 409) + assert.equal(bad.body.error, 'Keine gültige Domain') +})