aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_media.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-18 13:45:16 +0300
committergrm <grm@eyesin.space>2026-09-18 13:45:16 +0300
commit90578f02d851ab4e28a066404fbcf4be6a0ed9a7 (patch)
treed8e5f5fea70c0e8f11a93eebaa3d3add79b6c14a /internal/web/handlers_media.go
parent19353a51c869f4b24ef2253d856084b6e6728048 (diff)
downloadblogspace-90578f02d851ab4e28a066404fbcf4be6a0ed9a7.tar.gz
blogspace-90578f02d851ab4e28a066404fbcf4be6a0ed9a7.tar.bz2
blogspace-90578f02d851ab4e28a066404fbcf4be6a0ed9a7.zip
Security: Serve /media single-range only
http.ServeContent honours any number of comma-separated ranges and chunkReader caches one 512 KiB slice, so a Range header alternating between two chunks costs a substring() query per range: one 1 MB header could make Postgres read tens of gigabytes for a single anonymous request. Browsers and download managers only ever send one range, so a multi-range header is dropped and the file served whole. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/handlers_media.go')
-rw-r--r--internal/web/handlers_media.go13
1 files changed, 13 insertions, 0 deletions
diff --git a/internal/web/handlers_media.go b/internal/web/handlers_media.go
index 7c4b739..99fdb64 100644
--- a/internal/web/handlers_media.go
+++ b/internal/web/handlers_media.go
@@ -4,6 +4,7 @@ import (
"errors"
"io/fs"
"net/http"
+ "strings"
"github.com/google/uuid"
"github.com/gramanas/blogspace/internal/store"
@@ -38,9 +39,21 @@ func (s *Server) handleMedia(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Header().Set("ETag", etag)
w.Header().Set("X-Content-Type-Options", "nosniff")
+ singleRangeOnly(r)
http.ServeContent(w, r, f.Filename, f.CreatedAt, blogStore(r).FileReader(r.Context(), f))
}
+// singleRangeOnly drops a multi-range request so the file is served whole.
+// ServeContent honours any number of ranges and chunkReader caches one slice,
+// so a header alternating between two chunks would cost a substring() query
+// per range — tens of thousands per request. Browsers and download managers
+// only ever ask for one range.
+func singleRangeOnly(r *http.Request) {
+ if strings.Contains(r.Header.Get("Range"), ",") {
+ r.Header.Del("Range")
+ }
+}
+
// handleFavicon answers the browsers that ask for /favicon.ico regardless of
// the <link rel="icon"> tags: the blog's own icon if it set one, else the
// Blogspace default. Without this the request would render the 404 page.