Preview serves embedded EXIF thumbnail bytes verbatim without the nosniff/script-src hardening siblings apply
low Possibly Valid low confidence
Status
Possibly Valid
The verbatim attacker-byte passthrough plus the missing script-src 'none'/nosniff hardening (present on sibling handlers) is a real, code-proven content-injection surface. But the global default-src 'self' CSP contains script execution, and content-sniffing needs a minimal-mime host (the official image ships mailcap), so full XSS is gated and the residual is HTML/phishing injection — possibly_valid, low. Distinct from the rejected subtitle finding (DOK-100076), which had an explicit text/vtt type AND script-src 'none'. Canonical of the pair (100060).
Repository / Component
Plain-English Description
When making a small preview of a JPEG, the app can send back a thumbnail that is stored inside the uploaded file, exactly as the uploader wrote it, without the safety headers that similar download endpoints add. On an unusually configured server this raw content could be misinterpreted by the browser as a web page.
Description of the Underlying Issue
For the thumb size of JPEG-content files, img/service.go:167-176 (QualityLow branch) writes the embedded EXIF/IFD1 thumbnail (getEmbeddedThumbnail, service.go:218-259) to the response verbatim, with no re-encode. The preview handler (http/preview.go:107-108) sets only Cache-Control: private and relies on ServeContent for the Content-Type. It omits the Content-Security-Policy: script-src 'none' and X-Content-Type-Options: nosniff plus explicit Content-Type that the sibling handlers rawFileHandler (http/raw.go:220) and subtitleFileHandler (http/subtitle.go:61,64) set. For bmp/tiff filenames (FormatFromExtension, service.go:101-119) Go's builtin mime table has no entry, so ServeContent content-sniffs. This yields verbatim attacker-controlled bytes served with sniff-eligible typing and without the defense-in-depth headers used elsewhere.
Potential Attack
On a minimal-mime host (no /etc/mime.types), an attacker uploads evil.bmp whose bytes are a valid JPEG carrying a crafted EXIF thumbnail that is actually HTML. A victim opens the thumb preview (?inline=true). ServeContent, lacking a builtin type for .bmp and with nosniff absent, may sniff the verbatim thumbnail bytes as text/html and render them.
Outcomes of Potential Attack
Injection of attacker-controlled HTML/phishing content rendered same-origin. Full script-based XSS is contained by the global default-src 'self' CSP (http/http.go:32), so the residual is HTML/phishing injection or a same-origin script gadget, not inline/external script execution.
Affected Scope
GET /api/preview thumb size for JPEG-content files: the embedded EXIF thumbnail (attacker-controlled bytes) is served verbatim without the script-src 'none'/nosniff hardening applied to raw.go and subtitle.go.
Suggested Fix (plain english)
Add the same protective headers the download endpoints use to preview responses (block scripts, disable content sniffing, set an explicit image type), or re-encode the thumbnail instead of passing the uploaded bytes through unchanged.
Suggested Fix (detailed)
On preview responses (http/preview.go around 107-108) set Content-Security-Policy: script-src 'none', X-Content-Type-Options: nosniff, and an explicit image Content-Type, mirroring rawFileHandler (raw.go:220) and subtitleFileHandler (subtitle.go:61,64). Preferably also re-encode the embedded EXIF thumbnail (decode + re-emit as a known image format) rather than writing raw IFD1 bytes (service.go:167-176). Verify by requesting a crafted bmp thumb preview and confirming the response carries nosniff + an explicit image type and is not sniffed as HTML. Add Content-Security-Policy: script-src 'none', X-Content-Type-Options: nosniff, and an explicit image Content-Type on preview responses (mirror rawFileHandler/subtitleFileHandler); or re-encode the embedded thumbnail rather than passing it through verbatim.
Validation
The verbatim attacker-byte passthrough plus the missing script-src 'none'/nosniff hardening (present on sibling handlers) is a real, code-proven content-injection surface. But the global default-src 'self' CSP contains script execution, and content-sniffing needs a minimal-mime host (the official image ships mailcap), so full XSS is gated and the residual is HTML/phishing injection — possibly_valid, low. Distinct from the rejected subtitle finding (DOK-100076), which had an explicit text/vtt type AND script-src 'none'. Canonical of the pair (100060).
Full Evidence
img/service.go:167-176 QualityLow+JPEG branch out.Write(thm) emits embedded thumbnail verbatim (no re-encode)
img/service.go:218-259 getEmbeddedThumbnail returns raw IFD1 bytes
http/preview.go:107-108 ServeContent sets only Cache-Control: private; no script-src 'none', no nosniff, no explicit image Content-Type
http/raw.go:220 and http/subtitle.go:61,64 add script-src 'none' + explicit type — the sibling pattern preview omits
img/service.go:101-119 FormatFromExtension accepts bmp/tiff (absent from Go's builtin mime table → ServeContent sniffs)
http/http.go:32 preview (a matched /api route) DOES receive the global default-src 'self' CSP
Proven fact: Preview writes attacker-controlled EXIF thumbnail bytes verbatim
Proven fact: Preview omits the script-src 'none' + explicit non-sniffable Content-Type that raw.go/subtitle.go set
Proven fact: For bmp/tiff filenames ServeContent has no builtin MIME type and will content-sniff
Unvalidated fact: The preview response still carries the global CSP default-src 'self' (verified reachable), which blocks inline and cross-origin script execution — so a sniffed-HTML thumbnail cannot run inline/external XSS; only a same-origin script gadget or non-script HTML/phishing injection remains
Unvalidated fact: Content-sniffing to text/html requires a minimal-mime host: the official Docker image ships mailcap (/etc/mime.types), which supplies bmp/tiff types and defeats the sniff
img/service.go:167-176 QualityLow+JPEG branch out.Write(thm) emits the embedded thumbnail verbatim (no re-encode)
img/service.go:167-176 (QualityLow+FormatJpeg branch calls getEmbeddedThumbnail and out.Write(thm) with no re-encode/validation)
img/service.go:218-259 (getEmbeddedThumbnail returns raw IFD1 ifd.Thumbnail() bytes)
http/preview.go:107-108 (ServeContent sets only Cache-Control: private; no script-src none, no nosniff)
http/raw.go:220 and http/subtitle.go:61 (both ADD `Content-Security-Policy: script-src 'none';` when serving user content - the intended pattern preview breaks)
http/http.go:30-35 (global CSP is only `default-src 'self'; style-src 'unsafe-inline';`)
http/raw.go:73-81 (setContentDisposition honors attacker-supplied ?inline=true)
http/auth.go:57-62 and frontend/src/utils/auth.ts:13 (GET requests authenticate via SameSite=Strict `auth` cookie)
Dockerfile:28 (official image copies /etc/mime.types via mailcap - an external mitigation for the sniff step, absent on minimal custom builds)
img/service.go:101-119 (FormatFromExtension accepts bmp/tiff/tif) vs Go builtin mime table which omits them