From 41a03b9987f52244fb75df2efd96540321550971 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 12 Oct 2026 11:20:43 +0200 Subject: [PATCH] fix(14.2): WR-03 uploads route sandboxes files and downloads non-images The route answered with the blob's stored Content-Type, which attach.Store sniffs from the bytes, so a public text upload starting with was served as text/html on the application origin; nosniff does not help when the declared type is already HTML. Every response now carries a sandboxing Content-Security-Policy, and a successful response whose type is not a raster image (JPEG, PNG, GIF, WebP, AVIF) gets Content-Disposition: attachment, on GET and HEAD. Covers stay inline. --- docs/services/storage.md | 8 ++- docs/setup/configuration.md | 2 +- modules/surf/README.md | 4 +- modules/surf/uploads.go | 101 +++++++++++++++++++++++++++++------ modules/surf/uploads_test.go | 76 ++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 19 deletions(-) diff --git a/docs/services/storage.md b/docs/services/storage.md index a477539..7471ae3 100644 --- a/docs/services/storage.md +++ b/docs/services/storage.md @@ -51,7 +51,13 @@ storage: serve: true ``` -The route serves originals and thumbnails and answers 404 for a missing file, an invalid path and a file whose `system_files` row is not public (`is_public` false). Every response carries `X-Content-Type-Options: nosniff`, so a browser does not sniff a stored file into HTML or script. The route looks up the bucket and the database on each request: `route:list` lists it without opening either, and it answers 404 until `serve` has published them. The route owns the whole prefix, so start-up fails when a plugin registers a route at or under it. +The route serves originals and thumbnails and answers 404 for a missing file, an invalid path and a file whose `system_files` row is not public (`is_public` false). The stored content type is the one sniffed from the file's bytes at upload, so an uploaded text file that starts with `` is stored as `text/html`; `X-Content-Type-Options: nosniff` alone does not stop a browser from rendering a file whose declared type is already HTML. The route therefore protects the application origin in three ways: + +- Every response carries `X-Content-Type-Options: nosniff`. +- Every response carries `Content-Security-Policy: default-src 'none'; img-src 'self'; media-src 'self'; sandbox`. A file opened as a page runs no script and gets an opaque origin, so it cannot read the application's cookies or storage. The policy does not affect an `` or `