fix(19.3-03): name stored uploads after their sniffed content through attach.DiskExtension

- DiskExtension: a fixed table of specific sniffed types keeps a matching client
  extension or uses the canonical one; generic sniffs keep the client extension
  unless it is script-capable (then txt or bin); never asks package mime
- Store names the blob with DiskExtension(sniffed, ext); file_name, the allow-list
  and the stored content_type are unchanged
- TestStoreDiskExtensionSmoke: a PNG uploaded as x.html is stored and served as .png,
  report.docx keeps .docx, an HTML body named note.txt keeps .txt
- attachments docs on disk names, storage deploy note on nosniff and allow-lists,
  lagoon README
This commit is contained in:
Jakub Zych
2026-10-15 18:09:49 +02:00
parent 2382040b25
commit 2e22e9f9a6
6 changed files with 151 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
package attach_test
import (
"archive/zip"
"bytes"
"context"
"errors"
@@ -284,3 +285,61 @@ func TestStore(t *testing.T) {
}
})
}
// TestStoreDiskExtensionSmoke is the stored-XSS regression (D-15): the disk
// name's extension follows the sniffed content, file_name keeps the client's
// name, and documents whose sniff is generic keep their extension.
func TestStoreDiskExtensionSmoke(t *testing.T) {
if testing.Short() {
t.Skip("requires testcontainers postgres")
}
ctx := t.Context()
gdb := attachGorm(t)
if err := lagoon.Migrate(gdb, nil); err != nil {
t.Fatal(err)
}
bucket := memblob.OpenBucket(nil)
t.Cleanup(func() { _ = bucket.Close() })
lim := attach.Limits{Extensions: []string{"html", "png", "docx", "txt"}}
// An 8-byte PNG signature plus an IHDR chunk sniffs as image/png.
pngBody := append([]byte("\x89PNG\r\n\x1a\n"), []byte("\x00\x00\x00\x0dIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde")...)
f, err := attach.Store(ctx, gdb, bucket, attach.Upload{FileName: "x.html", Body: bytes.NewReader(pngBody), Public: true}, lim)
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(f.DiskName, ".png") || !strings.HasSuffix(f.URL(), ".png") {
t.Fatalf("PNG named x.html: disk name %q, URL %q, want .png", f.DiskName, f.URL())
}
if f.FileName != "x.html" || f.ContentType != "image/png" {
t.Fatalf("PNG named x.html: file_name %q, content_type %q", f.FileName, f.ContentType)
}
var zipBody bytes.Buffer
zw := zip.NewWriter(&zipBody)
w, err := zw.Create("word/document.xml")
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte("<w:document/>")); err != nil {
t.Fatal(err)
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
doc, err := attach.Store(ctx, gdb, bucket, attach.Upload{FileName: "report.docx", Body: bytes.NewReader(zipBody.Bytes())}, lim)
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(doc.DiskName, ".docx") || doc.FileName != "report.docx" {
t.Fatalf("docx: disk name %q, file_name %q", doc.DiskName, doc.FileName)
}
note, err := attach.Store(ctx, gdb, bucket, attach.Upload{FileName: "note.txt", Body: strings.NewReader("<html><body><script>alert(1)</script></body></html>")}, lim)
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(note.DiskName, ".txt") || note.FileName != "note.txt" {
t.Fatalf("HTML body named note.txt: disk name %q, file_name %q", note.DiskName, note.FileName)
}
}