Sandbox auf ausgelieferte Dateien, die Skript tragen koennen (d4rk-rp-planung#254)

Ein SVG mit <script> lief direkt geoeffnet auf der Herkunft des Dashboards
(FILES_HOST leer) - mit dessen Sitzung. Jeder Upload-Token konnte es ablegen,
die Arten-Fessel "Bild" laesst SVG durch.

Jetzt: Content-Security-Policy "sandbox; default-src 'none'; ..." auf allem
ausser Rasterbild, Video und Ton (Positivliste, dateiKopf in storage.ts), auf
allen Wegen: /f/, /t/, Dateiwirt, /api/privat. Nicht auf Video und Bild,
weil die Sandbox ein direkt geoeffnetes Video anhaelt (readyState 0 statt 4).

Gemessen mit tools/csp-pruefen.mjs gegen einen Wegwerf-Container aus diesem
Stand, Edge kopflos: vorher lief das Skript (Gegenprobe --erwarte-lauf),
jetzt 8/8 - Skript laeuft nicht, SVG wird gezeichnet, PNG und MP4 wie vorher,
Teilantwort 206.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
D4rkst3randClaude Opus 5.5 committed 2026-10-19 20:57:08 +02:00
1 parent 6630def7b2
commit a4e918be8a
4 files changed
+162 -7

No files matched your search

+4 -5
View File
@@ -30,7 +30,7 @@ import { config } from './config.js'
import { ensureAdmin } from './auth.js'
import { pruneEvents, pruneSessions, pruneVerwaltung, schreibeAbrufe, zaehleAbruf } from './db.js'
import { pruefePlatte, pruefeSicherung } from './meldung.js'
import { absolutePath, checkPath, lohntWebp, mimeFor, thumbPath, webpPfad } from './storage.js'
import { dateiKopf, absolutePath, checkPath, lohntWebp, mimeFor, thumbPath, webpPfad } from './storage.js'
import { uploadRoutes } from './routes/upload.js'
import { fivemanageRoutes } from './routes/fivemanage.js'
import { freigabeRoutes } from './routes/freigabe.js'
@@ -152,9 +152,8 @@ async function serveFile(c: any, rawPath: string) {
ETag: etag,
'Last-Modified': info.mtime.toUTCString(),
'Accept-Ranges': 'bytes',
// Kein Verlassen auf den Dateinamen: der Browser soll nicht raten,
// was er da vor sich hat.
'X-Content-Type-Options': 'nosniff',
// nosniff und die Sandbox gegen Skript-SVGs (#254) - storage.ts, dateiKopf.
...dateiKopf(typ),
}
// GEZAEHLT WIRD HIER, nach der Pruefung und vor der Auslieferung.
@@ -282,7 +281,7 @@ app.get('/t/*', async (c) => {
// Kurz: eine Vorschau aendert sich, wenn das Bild neu hochgeladen
// wird, und dann soll das Dashboard es sehen.
'Cache-Control': 'public, max-age=60',
'X-Content-Type-Options': 'nosniff',
...dateiKopf('image/webp'),
})
})
+3 -2
View File
@@ -33,6 +33,7 @@ import { fesselText, tokenAllows, tokenFromHeader } from '../auth.js'
import {
PathError,
checkPathUnter,
dateiKopf,
kannVorschau,
mimeFor,
privatPfad,
@@ -186,8 +187,8 @@ privatRoutes.get('/datei/:pfad{.+}', async (c) => {
return c.body(createReadStream(datei) as any, 200, {
'Content-Type': vorschau ? 'image/webp' : row.mime,
'Content-Length': String(groesse),
// Nie in einem geteilten Zwischenspeicher, und der Browser rät den Typ nicht.
// Nie in einem geteilten Zwischenspeicher; nosniff und Sandbox (#254) aus storage.ts.
'Cache-Control': 'private, no-store',
'X-Content-Type-Options': 'nosniff',
...dateiKopf(vorschau ? 'image/webp' : row.mime),
})
})
+33
View File
@@ -74,6 +74,39 @@ export function checkPathUnter(raw: string, basis: string): string {
return path
}
/**
* DIE KOPFZEILEN AUF JEDER AUSGELIEFERTEN DATEI (d4rk-rp-planung#254).
*
* Ein SVG kann <script> tragen, und es wird als image/svg+xml ausgeliefert
* -- direkt geoeffnet lief das Skript auf DIESER Herkunft, also da, wo das
* Dashboard mit seiner Sitzung wohnt (FILES_HOST ist leer). Gemessen
* 19.10.2026 mit tools/csp-pruefen.mjs: vorher lief es.
*
* `sandbox` schaltet Skripte im Dokument ab, auch im SVG. Fuer <img>, <video>
* und CSS-Hintergruende anderer Seiten aendert sich nichts: die Policy einer
* Antwort gilt nur, wenn sie SELBST das Dokument ist.
*
* NICHT AUF RASTERBILD, VIDEO UND TON: `sandbox` hielt ein direkt geoeffnetes
* Video an (readyState 0 statt 4, gemessen 19.10.2026 - auch mit media-src *,
* es liegt an der Sandbox selbst). Diese Arten fuehren ohnehin kein Skript aus,
* und nosniff verbietet dem Browser, sie umzudeuten. Die Sandbox bekommt
* deshalb ALLES ANDERE - eine Positivliste, keine Negativliste: eine neue Art,
* an die niemand gedacht hat, ist damit geschuetzt statt offen.
*
* EINE Stelle fuer alle Wege, die Bytes ausliefern (/f/, /t/, der Dateiwirt,
* /api/privat). Ein Weg ohne diese Kopfzeilen waere die Luecke von vorn.
*/
const OHNE_SANDBOX = /^(image\/(png|jpeg|gif|webp|avif)|video\/|audio\/)/i
const SANDBOX = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:"
/** Die Sicherheits-Kopfzeilen fuer eine Datei dieser Art (Content-Type). */
export function dateiKopf(typ: string): Record<string, string> {
return OHNE_SANDBOX.test(typ)
// Kein Verlassen auf den Dateinamen: der Browser soll nicht raten, was er vor sich hat.
? { 'X-Content-Type-Options': 'nosniff' }
: { 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': SANDBOX }
}
const MIME: Record<string, string> = {
webp: 'image/webp',
png: 'image/png',