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:
1 parent
6630def7b2
commit
a4e918be8a
4 files changed
+162
-7
No files matched your search
+4
-5
@@ -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'),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user