// Prueft, dass ausgelieferte Dateien kein Skript ausfuehren (d4rk-rp-planung#254) // und trotzdem noch als Bild und Video funktionieren. // // node tools/csp-pruefen.mjs // node tools/csp-pruefen.mjs http://localhost:9188 test/skript.svg test/bild.png test/clip.mp4 // // Gemessen wird im ECHTEN Browser (Edge kopflos, DevTools-Protokoll), nicht an // der Kopfzeile allein: ob ein SVG-Skript laeuft, entscheidet der Browser. // Das SVG setzt, wenn sein Skript laeuft, data-lief="ja" am Wurzelelement. // // GEGENPROBE: gegen einen Stand OHNE die Kopfzeile muss "svgSkriptLief" true // sein - sonst sieht diese Pruefung nichts und ihr "false" hiesse nichts. // Mit --erwarte-lauf prueft sie genau das (Rueckgabe 0, wenn das Skript LIEF). // // Das Edge-Profil: frisch je Lauf, aber mit der `Local State` einer Vorlage - // ein fremdes Profil kostet sonst eine Fehlanmeldung am Windows-Konto, zehn // davon sperren es (gemessen 12.10.2026, d4rk_mdt/frontend/tools/cdp.mjs, // dort steht dieselbe Logik und warum). import { spawn, spawnSync } from 'node:child_process' import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' const VORLAGE = join(process.env.LOCALAPPDATA ?? tmpdir(), 'd4rk-messen', 'edge-vorlage', 'Local State') const [basis, svg, png, mp4] = process.argv.slice(2).filter((a) => !a.startsWith('--')) const erwarteLauf = process.argv.includes('--erwarte-lauf') if (!basis || !svg || !png || !mp4) { console.error('Aufruf: node tools/csp-pruefen.mjs [--erwarte-lauf]') process.exit(2) } const schlaf = (ms) => new Promise((r) => setTimeout(r, ms)) // 1. Die Kopfzeilen, ohne Browser. const kopf = {} for (const [art, pfad] of [['svg', svg], ['png', png], ['mp4', mp4]]) { const r = await fetch(`${basis}/f/${pfad}`) kopf[art] = { status: r.status, typ: r.headers.get('content-type'), csp: r.headers.get('content-security-policy') } await r.arrayBuffer() } const teil = await fetch(`${basis}/f/${mp4}`, { headers: { Range: 'bytes=0-99' } }) kopf.mp4Teil = { status: teil.status, csp: teil.headers.get('content-security-policy') } await teil.arrayBuffer() // 2. Im Browser. const profil = mkdtempSync(join(tmpdir(), 'media-csp-')) try { copyFileSync(VORLAGE, join(profil, 'Local State')) } catch { /* noch keine Vorlage */ } const port = 9398 const edge = spawn(EDGE, ['--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profil}`, `--remote-debugging-port=${port}`, '--autoplay-policy=no-user-gesture-required', 'about:blank'], { stdio: 'ignore' }) let ws = null try { let url = null for (let i = 0; i < 60 && !url; i++) { try { url = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()).find((t) => t.type === 'page')?.webSocketDebuggerUrl } catch { /* noch nicht oben */ } if (!url) await schlaf(250) } if (!url) throw new Error('DevTools-Endpunkt kam nicht hoch') ws = new WebSocket(url) await new Promise((ok, weg) => { ws.onopen = ok; ws.onerror = () => weg(new Error('WebSocket zu')) }) let nr = 0 const offen = new Map() ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && offen.has(m.id)) { offen.get(m.id)(m); offen.delete(m.id) } } // Jeder Aufruf mit Zeitgrenze: ohne sie blieb ein unbeantworteter Aufruf ewig // stehen und der Lauf hing ohne jede Ausgabe (gemessen 19.10.2026). const cdp = (method, params = {}) => new Promise((ok) => { const id = ++nr offen.set(id, ok) ws.send(JSON.stringify({ id, method, params })) setTimeout(() => { if (offen.has(id)) { offen.delete(id); ok({ zeit: true }) } }, 10000) }) const werte = async (ausdruck) => { const r = await cdp('Runtime.evaluate', { expression: ausdruck, returnByValue: true, awaitPromise: true }) if (r.zeit) return 'ZEIT' if (r.result?.exceptionDetails) return 'FEHLER: ' + (r.result.exceptionDetails.exception?.description ?? r.result.exceptionDetails.text) return r.result?.result?.value } const oeffne = async (pfad, ms) => { console.error(`oeffne ${pfad}`); await cdp('Page.navigate', { url: `${basis}/f/${pfad}` }); await schlaf(ms) } await cdp('Page.enable'); await cdp('Runtime.enable') await oeffne(svg, 1200) const svgSkriptLief = (await werte(`document.documentElement.getAttribute('data-lief')`)) === 'ja' const svgGezeichnet = (await werte(`!!document.querySelector('rect')`)) === true await oeffne(png, 1000) const pngBreite = await werte(`document.querySelector('img')?.naturalWidth ?? null`) // OHNE Promise und Timer im Dokument gelesen: in einer Antwort mit `sandbox` // laufen keine Skript-Timer - ein awaitPromise mit setTimeout kam dort nie // zurueck (gemessen 19.10.2026, die erste Fassung hing daran). Gewartet wird // hier auf der Node-Seite. await oeffne(mp4, 3000) const video = await werte(`(() => { const v = document.querySelector('video'); return v ? { da: true, breite: v.videoWidth, hoehe: v.videoHeight, bereit: v.readyState } : { da: false } })()`) // Sandbox NUR auf dem, was Skript tragen kann (storage.ts, dateiKopf): auf // PNG und MP4 haelt sie das direkt geoeffnete Video an (gemessen 19.10.2026). const csp = (k) => typeof k?.csp === 'string' && k.csp.includes('sandbox') const pruefungen = erwarteLauf ? { svgSkriptLief: svgSkriptLief === true } : { svgSkriptLiefNicht: svgSkriptLief === false, svgGezeichnet, svgCsp: csp(kopf.svg), pngOhneSandbox: !csp(kopf.png), mp4OhneSandbox: !csp(kopf.mp4) && !csp(kopf.mp4Teil), pngBild: pngBreite === 1, videoSpielt: video?.da === true && video.breite > 0, mp4Teil206: kopf.mp4Teil.status === 206, } console.log(JSON.stringify({ kopf, svgSkriptLief, svgGezeichnet, pngBreite, video, pruefungen }, null, 1)) const rot = Object.entries(pruefungen).filter(([, v]) => !v).map(([k]) => k) console.log(rot.length ? `ROT: ${rot.join(', ')}` : `csp-pruefen: ${Object.keys(pruefungen).length}/${Object.keys(pruefungen).length} ok${erwarteLauf ? ' (Gegenprobe: Skript lief)' : ''}`) process.exitCode = rot.length ? 1 : 0 } finally { try { ws?.close() } catch { /* zu */ } spawnSync('taskkill', ['/T', '/F', '/PID', String(edge.pid)], { stdio: 'ignore' }) await schlaf(800) try { const inhalt = readFileSync(join(profil, 'Local State'), 'utf8'); JSON.parse(inhalt) mkdirSync(dirname(VORLAGE), { recursive: true }); writeFileSync(VORLAGE, inhalt) } catch { /* keine brauchbare Datei */ } for (let i = 0; i < 20; i++) { try { rmSync(profil, { recursive: true, force: true }); break } catch { await schlaf(500) } } }