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>
|
||
|---|---|---|
| .. | ||
| agency-layouts.js | ||
| agency-targets.js | ||
| apk-cache.js | ||
| apk-signature.js | ||
| app-settings.js | ||
| billing-token.js | ||
| billing.js | ||
| branding.js | ||
| chunked-prune.js | ||
| command-queue.js | ||
| content-ack-limiter.js | ||
| content-ingest.js | ||
| device-identity.js | ||
| device-sanitize.js | ||
| device-settings.js | ||
| emailVerify.js | ||
| flap-limiter.js | ||
| ghcr-check.js | ||
| image-gen.js | ||
| incident-classify.js | ||
| liveness.js | ||
| log-coalescer.js | ||
| media-orientation.js | ||
| ota-breaker.js | ||
| ota-download-guard.js | ||
| pair-lockout.js | ||
| permissions.js | ||
| player-media-health.js | ||
| reconnect-throttle.js | ||
| rolling-counter.js | ||
| safe-socket.js | ||
| schedule-eval.js | ||
| schema-check.js | ||
| secretbox.js | ||
| session-settle.js | ||
| socket-rooms.js | ||
| ssrf-guard.js | ||
| status-log-writer.js | ||
| tcp-keepalive.js | ||
| tenancy.js | ||
| tenant-cascade-migration.js | ||
| totp-lockout.js | ||
| totp.js | ||
| transition-bundle.js | ||
| transition-config.js | ||
| upload-sniff.js | ||
| user-deletion.js | ||
| wgt-cache.js | ||
| zone-validate.js | ||