mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Uploaded files are served from the SAME ORIGIN as the dashboard, so how a browser
interprets them is a security boundary. Two things decided that interpretation, and
both were caller-controlled: the stored extension came from
`path.extname(originalname)`, and the only type check read `file.mimetype` — a request
header. A caller could therefore choose to have their bytes served as an active
document from the app origin.
Two independent invariants now hold the boundary:
1. INGEST — lib/upload-sniff.js sniffs magic bytes after multer writes a neutral
`.part` file (diskStorage names the file before any bytes exist, so the sniff cannot
happen there), maps the result through a hardcoded mime->extension allowlist, renames
accordingly, and stores the sniffed mime. Unsupported bytes are refused with a 400.
2. SERVING — upload responses carry `Content-Security-Policy: sandbox`, so if a response
is ever treated as a document it lands in an opaque origin with scripts disabled.
Anything outside the inline-safe extension set is additionally forced to download.
This holds regardless of how a file reached disk, so a future gap in (1) is contained
rather than exploitable.
Applied at every instance of the pattern, not just the first: lib/content-ingest.js,
the /replace route, the four content-serving paths across server.js and routes/content.js
(the latter pair currently shadowed by mount order, which is not a guarantee), and the
ZIP-import path in routes/status.js, which took its extension from the archive entry.
SVG stays accepted and stays inline: white-label logos are SVG, and octet-stream +
nosniff makes <img> fail. Scripts in an SVG never run in an image context, and the
sandbox CSP covers the one case where they would — a direct navigation. SVG is also no
longer handed to sharp, which removes the librsvg path where the open libvips CVEs live.
Existing rows are untouched — no migration. The four upload fixtures in agency.test.js
uploaded `Buffer.from('x')` declared as image/png; that is the exact "declared type is a
lie" case this closes, so the fixtures now use real PNG bytes. No assertion changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| config | ||
| db | ||
| lib | ||
| middleware | ||
| player | ||
| routes | ||
| scripts | ||
| services | ||
| test | ||
| ws | ||
| .gitignore | ||
| config.js | ||
| package-lock.json | ||
| package.json | ||
| server.js | ||
| version.js | ||