package http import ( "mime" "strings" "testing" ) func TestContentDisposition(t *testing.T) { tests := []struct { name string filename string want string }{ {"plain", "roads.geojson", `attachment; filename="roads.geojson"; filename*=UTF-8''roads.geojson`}, {"space", "my map.tif", `attachment; filename="my map.tif"; filename*=UTF-8''my%20map.tif`}, {"quote injection", `a.tif"; filename="evil.exe`, `attachment; filename="a.tif_; filename=_evil.exe"; filename*=UTF-8''a.tif%22%3B%20filename%3D%22evil.exe`}, {"crlf injection", "a.tif\r\nSet-Cookie: x=1", `attachment; filename="a.tifSet-Cookie: x=1"; filename*=UTF-8''a.tifSet-Cookie%3A%20x%3D1`}, {"backslash", `..\..\a.sld`, `attachment; filename=".._.._a.sld"; filename*=UTF-8''..%5C..%5Ca.sld`}, {"cyrillic", "карта.tif", `attachment; filename="_____.tif"; filename*=UTF-8''%D0%BA%D0%B0%D1%80%D1%82%D0%B0.tif`}, {"empty", "", `attachment; filename="download"; filename*=UTF-8''download`}, {"only controls", "\r\n\t", `attachment; filename="download"; filename*=UTF-8''download`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := contentDisposition("attachment", tt.filename) if got != tt.want { t.Fatalf("contentDisposition(%q)\n got %s\nwant %s", tt.filename, got, tt.want) } if strings.ContainsAny(got, "\r\n") { t.Fatalf("header value contains CR/LF: %q", got) } // The value must parse as a single disposition whose decoded // filename is the sanitised original. disp, params, err := mime.ParseMediaType(got) if err != nil { t.Fatalf("ParseMediaType(%q): %v", got, err) } if disp != "attachment" { t.Fatalf("disposition = %q, want attachment", disp) } if len(params) != 1 { t.Fatalf("params = %v, want exactly one decoded filename", params) } }) } } func TestContentDispositionDecodesUTF8Name(t *testing.T) { _, params, err := mime.ParseMediaType(contentDisposition("inline", "карта 2026.cog.tif")) if err != nil { t.Fatal(err) } if got := params["filename"]; got != "карта 2026.cog.tif" { t.Fatalf("decoded filename = %q", got) } }