fix(lagoon/attach): StaticHandler sends the stored Content-Disposition and nosniff (quick-261105-oc8)
- attach.StaticHandler and attach.StaticHandlerPublic set X-Content-Type-Options: nosniff on every response - the stored disposition is read with one Attributes call after the is_public gate and sent on GET and HEAD; a value mime.ParseMediaType rejects is sent as attachment - blobs without a stored disposition are served as before - lagoon README, storage and attachments docs pages, surf uploads comment updated
This commit is contained in:
@@ -3,6 +3,7 @@ package attach
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -23,12 +24,22 @@ const defaultStaticContentType = "application/octet-stream"
|
||||
// disk). To 404 is_public=false rows, mount StaticHandlerPublic instead.
|
||||
// Do not mount the ungated handler on the app origin — same-origin
|
||||
// Content-Type from uploads is XSS-relevant.
|
||||
//
|
||||
// Every response carries X-Content-Type-Options: nosniff. A served file gets
|
||||
// its stored Content-Type and, when the blob has one, its stored
|
||||
// Content-Disposition: Store stores attachment for every type
|
||||
// IsInlineContentType rejects, and a stored value mime.ParseMediaType rejects
|
||||
// is sent as attachment. The handler derives no disposition from the type and
|
||||
// sends no Content-Security-Policy, so a blob stored without a disposition is
|
||||
// served inline with its type. Reading the disposition costs one Attributes
|
||||
// call per request.
|
||||
func StaticHandler(bucket *blob.Bucket, prefix string) http.Handler {
|
||||
return servePublicBlobs(bucket, prefix, nil)
|
||||
}
|
||||
|
||||
// StaticHandlerPublic is StaticHandler plus an is_public gate. Missing
|
||||
// rows and is_public=false both 404. The lookup runs before NewReader.
|
||||
// It sends the same headers as StaticHandler.
|
||||
func StaticHandlerPublic(bucket *blob.Bucket, prefix string, db *gorm.DB) http.Handler {
|
||||
return servePublicBlobs(bucket, prefix, func(ctx context.Context, filename string) (bool, error) {
|
||||
return fileIsPublic(ctx, db, filename)
|
||||
@@ -42,6 +53,7 @@ func servePublicBlobs(bucket *blob.Bucket, prefix string, allow func(context.Con
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
if bucket == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
@@ -63,6 +75,11 @@ func servePublicBlobs(bucket *blob.Bucket, prefix string, allow func(context.Con
|
||||
return
|
||||
}
|
||||
}
|
||||
attrs, err := bucket.Attributes(r.Context(), key)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
reader, err := bucket.NewReader(r.Context(), key, nil)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
@@ -74,6 +91,9 @@ func servePublicBlobs(bucket *blob.Bucket, prefix string, allow func(context.Con
|
||||
ct = defaultStaticContentType
|
||||
}
|
||||
w.Header().Set("Content-Type", ct)
|
||||
if cd := storedDisposition(attrs.ContentDisposition); cd != "" {
|
||||
w.Header().Set("Content-Disposition", cd)
|
||||
}
|
||||
if r.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
@@ -81,6 +101,21 @@ func servePublicBlobs(bucket *blob.Bucket, prefix string, allow func(context.Con
|
||||
})
|
||||
}
|
||||
|
||||
// storedDisposition is the value of the stored Content-Disposition a handler
|
||||
// sends: empty for none, the trimmed value when mime.ParseMediaType accepts
|
||||
// it, and attachment for a value it rejects, so malformed or injected
|
||||
// metadata never reaches a response header.
|
||||
func storedDisposition(raw string) string {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
if _, _, err := mime.ParseMediaType(v); err != nil {
|
||||
return attachmentDisposition
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func stripStaticPrefix(path, prefix string) (string, bool) {
|
||||
if prefix == "" {
|
||||
return strings.TrimPrefix(path, "/"), true
|
||||
|
||||
Reference in New Issue
Block a user