Repository: gohugoio/hugo (language: go)

Issue:
Allow whitelisting mediaTypes used in resources.GetRemote
<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.103.0+extended darwin/amd64 BuildDate=unknown
</pre>

### Does this issue reproduce with the latest release?
yes


Hi there

I got a build error - `resources.GetRemote : failed to resolve media type for remote resource `

while trying to get remote resources with content types of MS Office files:

- .doc     `application/msword`
- .docx     `application/vnd.openxmlformats-officedocument.wordprocessingml.document`
- .xls     ` application/vnd.ms-excel`
- .xlsx     `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
- .ppt    ` application/vnd.ms-powerpoint`
- .pptx    ` application/vnd.openxmlformats-officedocument.presentationml.presentation`

MediaType are also added to config file and with other additional MediaTypes like zip `application/zip` `resources.GetRemote ` works ok. Trying to add Options with "Content-Type" to GetRemote request have no effect.


P.S.
also found mistake in [docs](https://gohugo.io/content-management/page-resources/#properties)
correct mime type for PowerPoint  `application/vnd.ms-powerpoint`, not  `application/vnd.mspowerpoint`



Discussion / hints:
I encounter the same problem, on hugo 0.108.0, with `image/avif` media type. (.avif)

Adding it to config.yaml does not help (same error message)
```yaml
mediaTypes:
  image/avif:
    suffixes:
      - avif
```
The mime TYPE detection in GetRemote is a little bit restrictive, we don't just look at the suffix. This comes from security concerns, but that means that in its current form there will always be reports like the above.

So I suggest that we allow setting the MediaType directly:

```
{{ $r := resources.GetRemote "https://example.org" (dict "mediaType" "application/msword") }}
```
@bep Would you consider this?

```
[security.http]
methods = ['(?i)GET|POST']
urls = ['.*']
mediaTypes = ['image/avif','^application/vnd.openxmlformats']
```

This would represent _additional_ media types, so the naming may not be great.


@jmooring yea, that would probably be a better idea. I don't have a better name on the tip of my tongue.


@jmooring OK, thanks for bringing this to my attention. Looking back at now, I don't see why we couldn't simply do:

```
{{ $r := resources.GetRemote "https://example.org" (dict "mediaType" "application/msword") }}
```

I don't see any obvious security implications in the above???
If we do this...

```
{{ $r := resources.GetRemote "https://example.org" (dict "mediaType" "application/msword") }}
```

...we would have to know the expected media type in the response header  _before_ making the request. That wouldn't work with this (contrived) example:

content/about.md

```
+++
title = 'About'
download = 'https://example.org/a.foo'
+++
```

```text
{{ with .Params.download }}
  {{ if (urls.Parse .).IsAbs }}
    {{ with resources.GetRemote . }}
      <a href="{{ .RelPermalink }}" download>Download</a>
    {{ end }}
  {{ else }}
    ...
  {{ end }}
{{ end }}
```

So I'm back to...

```
[security.http]
methods = ['(?i)GET|POST']
urls = ['.*']
mediaTypes = ['image/avif','^application/vnd.openxmlformats']
```

or similar.
>...we would have to know the expected media type in the response header before making the request. That wouldn't work with this (contrived) example:

OK, now I think I get it; so in the examples above:

1. The response header is set to a sensible mime type.
2. But the content sniffing fails to resolve to that same mime type?

Is that correct? So an additional security config would whitelist some types saying we trust the response header value?
> Is that correct?

Yes.

> So an additional security config would whitelist some types saying we trust the response header value?

Yes.

But I think we need to remember that, based on past behavior, someone will inevitably will do this:

```text
[security.http]
mediaTypes = ['.*']
```

But if you aim for your own foot, I think you deserve the consequences.



>But if you aim for your own foot, I think you deserve the consequences.

I said this before, but: The security config is there mainly to avoid ... spring-guns.

Fix patch:
diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go
index 66e89fb97ff..f7d2beac8bf 100644
--- a/config/security/securityConfig.go
+++ b/config/security/securityConfig.go
@@ -88,6 +88,9 @@ type HTTP struct {
 
 	// HTTP methods to allow.
 	Methods Whitelist `json:"methods"`
+
+	// Media types where the Content-Type in the response is used instead of resolving from the file content.
+	MediaTypes Whitelist `json:"mediaTypes"`
 }
 
 // ToTOML converts c to TOML with [security] as the root.
diff --git a/resources/resource_factories/create/remote.go b/resources/resource_factories/create/remote.go
index 3aae57e8d3a..73171e57075 100644
--- a/resources/resource_factories/create/remote.go
+++ b/resources/resource_factories/create/remote.go
@@ -171,10 +171,17 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou
 
 	contentType := res.Header.Get("Content-Type")
 
-	if isHeadMethod {
-		// We have no body to work with, so we need to use the Content-Type header.
-		mediaType, _ = media.FromString(contentType)
-	} else {
+	// For HEAD requests we have no body to work with, so we need to use the Content-Type header.
+	if isHeadMethod || c.rs.ExecHelper.Sec().HTTP.MediaTypes.Accept(contentType) {
+		var found bool
+		mediaType, found = c.rs.MediaTypes().GetByType(contentType)
+		if !found {
+			// A media type not configured in Hugo, just create one from the content type string.
+			mediaType, _ = media.FromString(contentType)
+		}
+	}
+
+	if mediaType.IsZero() {
 
 		var extensionHints []string
 


Test patch:
diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go
index 55409e318bf..edc1737e34a 100644
--- a/config/security/securityConfig_test.go
+++ b/config/security/securityConfig_test.go
@@ -163,8 +163,10 @@ func TestDecodeConfigDefault(t *testing.T) {
 	c.Assert(pc.HTTP.Methods.Accept("GET"), qt.IsTrue)
 	c.Assert(pc.HTTP.Methods.Accept("get"), qt.IsTrue)
 	c.Assert(pc.HTTP.Methods.Accept("DELETE"), qt.IsFalse)
+	c.Assert(pc.HTTP.MediaTypes.Accept("application/msword"), qt.IsFalse)
 
 	c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue)
 	c.Assert(pc.Exec.OsEnv.Accept("GOROOT"), qt.IsTrue)
 	c.Assert(pc.Exec.OsEnv.Accept("MYSECRET"), qt.IsFalse)
+
 }
diff --git a/hugolib/securitypolicies_test.go b/hugolib/securitypolicies_test.go
index aa062bb1f9d..5b9267b591d 100644
--- a/hugolib/securitypolicies_test.go
+++ b/hugolib/securitypolicies_test.go
@@ -138,9 +138,9 @@ func TestSecurityPolicies(t *testing.T) {
 		}
 		cb := func(b *sitesBuilder) {
 			b.WithConfigFile("toml", `
-			[security]
-			[security.exec]
-			allow="none"	
+[security]
+[security.exec]
+allow="none"	
 		
 			`)
 			b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss"  | resources.ToCSS (dict "transpiler" "dartsass") }}`)
@@ -166,6 +166,28 @@ func TestSecurityPolicies(t *testing.T) {
 [security]		
 [security.http]
 urls="none"
+`)
+			})
+	})
+
+	c.Run("resources.GetRemote, fake JSON", func(c *qt.C) {
+		c.Parallel()
+		httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fakejson.json" }}{{ $json.Content }}`, `(?s).*failed to resolve media type.*`,
+			func(b *sitesBuilder) {
+				b.WithConfigFile("toml", `
+`)
+			})
+	})
+
+	c.Run("resources.GetRemote, fake JSON whitelisted", func(c *qt.C) {
+		c.Parallel()
+		httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fakejson.json" }}{{ $json.Content }}`, ``,
+			func(b *sitesBuilder) {
+				b.WithConfigFile("toml", `
+[security]		
+[security.http]
+mediaTypes=["application/json"]
+
 `)
 			})
 	})
diff --git a/hugolib/testdata/fakejson.json b/hugolib/testdata/fakejson.json
new file mode 100644
index 00000000000..f191b280ce9
Binary files /dev/null and b/hugolib/testdata/fakejson.json differ