- 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
79 lines
3.1 KiB
Go
79 lines
3.1 KiB
Go
package attach
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// sniffedExtensions maps a specific sniffed content type to the extensions
|
|
// that may name it on disk; the first entry is the canonical one. The table
|
|
// is fixed in code on purpose: package mime's answers depend on the host's
|
|
// mime tables.
|
|
var sniffedExtensions = map[string][]string{
|
|
"image/png": {"png"},
|
|
"image/jpeg": {"jpg", "jpeg", "jpe"},
|
|
"image/gif": {"gif"},
|
|
"image/webp": {"webp"},
|
|
"image/bmp": {"bmp"},
|
|
"image/x-icon": {"ico"},
|
|
"image/vnd.microsoft.icon": {"ico"},
|
|
"application/pdf": {"pdf"},
|
|
"audio/mpeg": {"mp3"},
|
|
"audio/wave": {"wav"},
|
|
"audio/wav": {"wav"},
|
|
"application/ogg": {"ogg", "oga", "opus"},
|
|
"audio/ogg": {"ogg", "oga", "opus"},
|
|
"video/webm": {"webm", "mkv"},
|
|
"video/mp4": {"mp4", "m4v"},
|
|
"font/woff2": {"woff2"},
|
|
"font/woff": {"woff"},
|
|
"font/ttf": {"ttf"},
|
|
"font/otf": {"otf"},
|
|
"application/vnd.ms-fontobject": {"eot"},
|
|
"application/x-rar-compressed": {"rar"},
|
|
"application/x-gzip": {"gz", "tgz"},
|
|
}
|
|
|
|
// scriptCapableExtensions are extensions a browser or web server may run or
|
|
// render as a document. They are never kept on disk under a generic sniff.
|
|
var scriptCapableExtensions = []string{
|
|
"html", "htm", "xhtml", "xht", "shtml", "svg", "svgz", "xml", "xsl", "xslt",
|
|
"js", "mjs", "css", "swf", "php", "phtml", "pht", "phar",
|
|
}
|
|
|
|
// DiskExtension returns the extension, without the dot, under which an
|
|
// upload is stored, so a web server that picks the Content-Type from the
|
|
// extension serves what the bytes are rather than what the client named
|
|
// them. contentType is the sniffed base media type (http.DetectContentType
|
|
// without parameters) and clientExt the client's allow-listed extension.
|
|
//
|
|
// For a specific sniffed type (PNG, JPEG, GIF, WebP, PDF, MP3, WebM, fonts
|
|
// and so on) the client extension is kept when it belongs to that type,
|
|
// else the type's canonical extension is used: a PNG named x.html is stored
|
|
// as .png. Any other type is generic (application/octet-stream,
|
|
// application/zip, text/plain, text/html, text/xml): the client extension
|
|
// is kept, so .docx, .xlsx and .ics survive, unless it is script-capable
|
|
// (html, svg, xml, js, css, php and the like), which becomes txt for a
|
|
// text/* type and bin otherwise.
|
|
func DiskExtension(contentType, clientExt string) string {
|
|
ct := strings.ToLower(strings.TrimSpace(contentType))
|
|
if base, _, ok := strings.Cut(ct, ";"); ok {
|
|
ct = strings.TrimSpace(base)
|
|
}
|
|
ext := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(clientExt), "."))
|
|
|
|
if exts, ok := sniffedExtensions[ct]; ok {
|
|
if slices.Contains(exts, ext) {
|
|
return ext
|
|
}
|
|
return exts[0]
|
|
}
|
|
if extPattern.MatchString(ext) && !slices.Contains(scriptCapableExtensions, ext) {
|
|
return ext
|
|
}
|
|
if strings.HasPrefix(ct, "text/") {
|
|
return "txt"
|
|
}
|
|
return "bin"
|
|
}
|