diff --git a/README.md b/README.md index a7f62ec..326e62c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ Downloads support range requests, so an interrupted transfer can be resumed: curl -C - -O https://beam.example.com/x123abcx/myfile.txt ``` +Uploaded content is served with `X-Content-Type-Options: nosniff` and +`Content-Security-Policy: sandbox`, so an uploaded HTML or SVG file opens as a +sandboxed, script-free document instead of running on the beam origin. + ### Endpoints | Method | Path | Purpose | diff --git a/main.go b/main.go index 5addbbc..8b45600 100644 --- a/main.go +++ b/main.go @@ -466,6 +466,14 @@ func handleDownload(w http.ResponseWriter, r *http.Request) { // The extension lookup relies on the system MIME database, which the // Dockerfile provides via the mailcap package. w.Header().Set("Content-Type", detectContentType(filename)) + // Uploads are anonymous and served back from this origin, so an uploaded + // .html or .svg opened inline would otherwise be stored XSS. The sandbox + // directive puts the response in an opaque origin with scripting, forms and + // top-level navigation disabled; nosniff stops a mislabeled file from being + // re-typed into something executable. Both are set before ServeContent so + // they apply to its 206, 304 and HEAD responses too. + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Content-Security-Policy", "sandbox") // FormatMediaType returns "" for names it cannot encode; don't emit an // empty header in that case. if disposition := mime.FormatMediaType("inline", map[string]string{"filename": filename}); disposition != "" {