Always use content to resolve content type in resources.GetRemote

This is a security hardening measure; don't trust the URL extension or any `Content-Type`/`Content-Disposition` header on its own, always look at the file content using Go's `http.DetectContentType`.

This commit also adds ttf and otf media type definitions to Hugo.

Fixes #9302
Fixes #9301
This commit is contained in:
Bjørn Erik Pedersen
2021-12-16 15:12:13 +01:00
parent 22ef5da20d
commit 44954497bc
26 changed files with 378 additions and 49 deletions

View File

@@ -20,6 +20,7 @@ import (
"strings"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/media"
"github.com/pkg/errors"
@@ -45,6 +46,15 @@ var (
".webp": WEBP,
}
imageFormatsBySubType = map[string]Format{
media.JPEGType.SubType: JPEG,
media.PNGType.SubType: PNG,
media.TIFFType.SubType: TIFF,
media.BMPType.SubType: BMP,
media.GIFType.SubType: GIF,
media.WEBPType.SubType: WEBP,
}
// Add or increment if changes to an image format's processing requires
// re-generation.
imageFormatsVersions = map[Format]int{
@@ -102,6 +112,11 @@ func ImageFormatFromExt(ext string) (Format, bool) {
return f, found
}
func ImageFormatFromMediaSubType(sub string) (Format, bool) {
f, found := imageFormatsBySubType[sub]
return f, found
}
const (
defaultJPEGQuality = 75
defaultResampleFilter = "box"

View File

@@ -66,6 +66,9 @@ func (*Filters) Text(text string, options ...interface{}) gift.Filter {
case "linespacing":
tf.linespacing = cast.ToInt(v)
case "font":
if err, ok := v.(error); ok {
panic(fmt.Sprintf("invalid font source: %s", err))
}
fontSource, ok1 := v.(hugio.ReadSeekCloserProvider)
identifier, ok2 := v.(resource.Identifier)

View File

@@ -36,6 +36,7 @@ func TestCreatePlaceholders(t *testing.T) {
"Suffixes": "pre_foo.Suffixes_post",
"Delimiter": "pre_foo.Delimiter_post",
"FirstSuffix": "pre_foo.FirstSuffix_post",
"IsText": "pre_foo.IsText_post",
"String": "pre_foo.String_post",
"Type": "pre_foo.Type_post",
"MainType": "pre_foo.MainType_post",

View File

@@ -69,6 +69,9 @@ type ResourceSourceDescriptor struct {
Fs afero.Fs
// Set when its known up front, else it's resolved from the target filename.
MediaType media.Type
// The relative target filename without any language code.
RelTargetFilename string

View File

@@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/mitchellh/mapstructure"
@@ -99,7 +100,7 @@ func (c *Client) FromRemote(uri string, optionsm map[string]interface{}) (resour
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, errors.Wrapf(err, "failed to read remote resource %s", uri)
return nil, errors.Wrapf(err, "failed to read remote resource %q", uri)
}
filename := path.Base(rURL.Path)
@@ -109,33 +110,30 @@ func (c *Client) FromRemote(uri string, optionsm map[string]interface{}) (resour
}
}
var extension string
var extensionHint string
if arr, _ := mime.ExtensionsByType(res.Header.Get("Content-Type")); len(arr) == 1 {
extension = arr[0]
extensionHint = arr[0]
}
// If extension was not determined by header, look for a file extention
if extension == "" {
// Look for a file extention
if extensionHint == "" {
if ext := path.Ext(filename); ext != "" {
extension = ext
extensionHint = ext
}
}
// If extension was not determined by header or file extention, try using content itself
if extension == "" {
if ct := http.DetectContentType(body); ct != "application/octet-stream" {
if ct == "image/jpeg" {
extension = ".jpg"
} else if arr, _ := mime.ExtensionsByType(ct); arr != nil {
extension = arr[0]
}
}
// Now resolve the media type primarily using the content.
mediaType := media.FromContent(c.rs.MediaTypes, extensionHint, body)
if mediaType.IsZero() {
return nil, errors.Errorf("failed to resolve media type for remote resource %q", uri)
}
resourceID = filename[:len(filename)-len(path.Ext(filename))] + "_" + resourceID + extension
resourceID = filename[:len(filename)-len(path.Ext(filename))] + "_" + resourceID + mediaType.FirstSuffix.FullSuffix
return c.rs.New(
resources.ResourceSourceDescriptor{
MediaType: mediaType,
LazyPublish: true,
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
return hugio.NewReadSeekerNoOpCloser(bytes.NewReader(body)), nil

View File

@@ -272,21 +272,28 @@ func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (reso
fd.RelTargetFilename = sourceFilename
}
ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename))
mimeType, suffixInfo, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, "."))
// TODO(bep) we need to handle these ambiguous types better, but in this context
// we most likely want the application/xml type.
if suffixInfo.Suffix == "xml" && mimeType.SubType == "rss" {
mimeType, found = r.MediaTypes.GetByType("application/xml")
}
mimeType := fd.MediaType
if mimeType.IsZero() {
ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename))
var (
found bool
suffixInfo media.SuffixInfo
)
mimeType, suffixInfo, found = r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, "."))
// TODO(bep) we need to handle these ambiguous types better, but in this context
// we most likely want the application/xml type.
if suffixInfo.Suffix == "xml" && mimeType.SubType == "rss" {
mimeType, found = r.MediaTypes.GetByType("application/xml")
}
if !found {
// A fallback. Note that mime.TypeByExtension is slow by Hugo standards,
// so we should configure media types to avoid this lookup for most
// situations.
mimeStr := mime.TypeByExtension(ext)
if mimeStr != "" {
mimeType, _ = media.FromStringAndExt(mimeStr, ext)
if !found {
// A fallback. Note that mime.TypeByExtension is slow by Hugo standards,
// so we should configure media types to avoid this lookup for most
// situations.
mimeStr := mime.TypeByExtension(ext)
if mimeStr != "" {
mimeType, _ = media.FromStringAndExt(mimeStr, ext)
}
}
}
@@ -301,7 +308,7 @@ func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (reso
mimeType)
if mimeType.MainType == "image" {
imgFormat, ok := images.ImageFormatFromExt(ext)
imgFormat, ok := images.ImageFormatFromMediaSubType(mimeType.SubType)
if ok {
ir := &imageResource{
Image: images.NewImage(imgFormat, r.imaging, nil, gr),