diff options
Diffstat (limited to 'workhorse/internal')
3 files changed, 85 insertions, 7 deletions
diff --git a/workhorse/internal/headers/content_headers.go b/workhorse/internal/headers/content_headers.go index 854cc8abddd..54c7c1bdd95 100644 --- a/workhorse/internal/headers/content_headers.go +++ b/workhorse/internal/headers/content_headers.go @@ -1,6 +1,7 @@ package headers import ( + "mime" "net/http" "regexp" @@ -13,8 +14,9 @@ var ( imageTypeRegex = regexp.MustCompile(`^image/*`) svgMimeTypeRegex = regexp.MustCompile(`^image/svg\+xml$`) - textTypeRegex = regexp.MustCompile(`^text/*`) - + textTypeRegex = regexp.MustCompile(`^text/*`) + xmlTypeRegex = regexp.MustCompile(`^text/xml`) + xhtmlTypeRegex = regexp.MustCompile(`^text/html`) videoTypeRegex = regexp.MustCompile(`^video/*`) pdfTypeRegex = regexp.MustCompile(`application\/pdf`) @@ -26,6 +28,8 @@ var ( // Mime types that can't be inlined. Usually subtypes of main types var forbiddenInlineTypes = []*regexp.Regexp{svgMimeTypeRegex} +var htmlRenderingTypes = []*regexp.Regexp{xmlTypeRegex, xhtmlTypeRegex} + // Mime types that can be inlined. We can add global types like "image/" or // specific types like "text/plain". If there is a specific type inside a global // allowed type that can't be inlined we must add it to the forbiddenInlineTypes var. @@ -38,12 +42,28 @@ const ( textPlainContentType = "text/plain; charset=utf-8" attachmentDispositionText = "attachment" inlineDispositionText = "inline" + dummyFilename = "blob" ) func SafeContentHeaders(data []byte, contentDisposition string) (string, string) { - contentType := safeContentType(data) + detectedContentType := detectContentType(data) + + contentType := safeContentType(detectedContentType) contentDisposition = safeContentDisposition(contentType, contentDisposition) + // Some browsers will render XML inline unless a filename directive is provided with a non-xml file extension + // This overrides the filename directive in the case of XML data + for _, element := range htmlRenderingTypes { + if isType(detectedContentType, element) { + disposition, directives, err := mime.ParseMediaType(contentDisposition) + if err == nil { + directives["filename"] = dummyFilename + contentDisposition = mime.FormatMediaType(disposition, directives) + break + } + } + } + // Set attachments to application/octet-stream since browsers can do // a better job distinguishing certain types (for example: ZIP files // vs. Microsoft .docx files). However, browsers may safely render SVGs even @@ -56,15 +76,17 @@ func SafeContentHeaders(data []byte, contentDisposition string) (string, string) return contentType, contentDisposition } -func safeContentType(data []byte) string { +func detectContentType(data []byte) string { // Special case for svg because DetectContentType detects it as text if svg.Is(data) { return svgContentType } // Override any existing Content-Type header from other ResponseWriters - contentType := http.DetectContentType(data) + return http.DetectContentType(data) +} +func safeContentType(contentType string) string { // http.DetectContentType does not support JavaScript and would only // return text/plain. But for cautionary measures, just in case they start supporting // it down the road and start returning application/javascript, we want to handle it now diff --git a/workhorse/internal/headers/content_headers_test.go b/workhorse/internal/headers/content_headers_test.go new file mode 100644 index 00000000000..7cfce335d88 --- /dev/null +++ b/workhorse/internal/headers/content_headers_test.go @@ -0,0 +1,56 @@ +package headers + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func fileContents(fileName string) []byte { + fileContents, _ := os.ReadFile(fileName) + return fileContents +} + +func TestHeaders(t *testing.T) { + tests := []struct { + desc string + fileContents []byte + expectedContentType string + expectedContentDisposition string + }{ + { + desc: "XML file", + fileContents: fileContents("../../testdata/test.xml"), + expectedContentType: "text/plain; charset=utf-8", + expectedContentDisposition: "inline; filename=blob", + }, + { + desc: "XHTML file", + fileContents: fileContents("../../testdata/index.xhtml"), + expectedContentType: "text/plain; charset=utf-8", + expectedContentDisposition: "inline; filename=blob", + }, + { + desc: "svg+xml file", + fileContents: fileContents("../../testdata/xml.svg"), + expectedContentType: "image/svg+xml", + expectedContentDisposition: "attachment", + }, + { + desc: "text file", + fileContents: []byte(`a text file`), + expectedContentType: "text/plain; charset=utf-8", + expectedContentDisposition: "inline", + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + contentType, newContentDisposition := SafeContentHeaders(test.fileContents, "") + + require.Equal(t, test.expectedContentType, contentType) + require.Equal(t, test.expectedContentDisposition, newContentDisposition) + }) + } +} diff --git a/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go b/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go index b04263de6b9..e863935be6f 100644 --- a/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go +++ b/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go @@ -51,13 +51,13 @@ func TestSetProperContentTypeAndDisposition(t *testing.T) { { desc: "HTML type", contentType: "text/plain; charset=utf-8", - contentDisposition: "inline", + contentDisposition: "inline; filename=blob", body: "<html><body>Hello world!</body></html>", }, { desc: "Javascript within HTML type", contentType: "text/plain; charset=utf-8", - contentDisposition: "inline", + contentDisposition: "inline; filename=blob", body: "<script>alert(\"foo\")</script>", }, { |