mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
79f030be5b
Whitelist now treats any pattern prefixed with "! " (the same negation
prefix used by hglob/predicate) as a deny rule. Deny matches take
precedence over allow, and a whitelist made up exclusively of deny
rules implicitly allows everything it does not deny.
The default security.http.urls now reads:
urls = ['(?i)^https?://[a-z]', '! (?i)localhost', '! @']
i.e. allow URLs whose host starts with a letter (the common
"https://example.com" shape), deny anything that looks like localhost,
and deny URLs with userinfo to foil "http://user@127.0.0.1/" bypasses.
Public IP literals are collateral blocks; users who need them (or their
own private hosts) override security.http.urls as before, mixing allow
and deny rules with the same "! " prefix, e.g.
[security.http]
urls = ['.*', '! ^https?://evil\.example\.com']
Fixes #14792
259 lines
6.6 KiB
Go
259 lines
6.6 KiB
Go
// Copyright 2025 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package hugolib
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"runtime"
|
|
"testing"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/gohugoio/hugo/markup/asciidocext"
|
|
"github.com/gohugoio/hugo/markup/pandoc"
|
|
"github.com/gohugoio/hugo/markup/rst"
|
|
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
|
|
)
|
|
|
|
func TestSecurityPolicies(t *testing.T) {
|
|
c := qt.New(t)
|
|
|
|
c.Run("os.GetEnv, denied", func(c *qt.C) {
|
|
c.Parallel()
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- layouts/home.html --
|
|
{{ os.Getenv "FOOBAR" }}
|
|
`
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*"FOOBAR" is not whitelisted in policy "security\.funcs\.getenv".*`)
|
|
})
|
|
|
|
c.Run("os.GetEnv, OK", func(c *qt.C) {
|
|
c.Parallel()
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- layouts/home.html --
|
|
{{ os.Getenv "HUGO_FOO" }}
|
|
`
|
|
Test(c, files)
|
|
})
|
|
|
|
c.Run("AsciiDoc, denied", func(c *qt.C) {
|
|
c.Parallel()
|
|
if ok, err := asciidocext.Supports(); !ok {
|
|
c.Skip(err)
|
|
}
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- content/page.ad --
|
|
foo
|
|
`
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*"asciidoctor" is not whitelisted in policy "security\.exec\.allow".*`)
|
|
})
|
|
|
|
c.Run("RST, denied", func(c *qt.C) {
|
|
c.Parallel()
|
|
if !rst.Supports() {
|
|
c.Skip()
|
|
}
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- content/page.rst --
|
|
foo
|
|
`
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
if runtime.GOOS == "windows" {
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*python(\.exe)?" is not whitelisted in policy "security\.exec\.allow".*`)
|
|
} else {
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*"rst2html(\.py)?" is not whitelisted in policy "security\.exec\.allow".*`)
|
|
}
|
|
})
|
|
|
|
c.Run("Pandoc, denied", func(c *qt.C) {
|
|
c.Parallel()
|
|
if !pandoc.Supports() {
|
|
c.Skip()
|
|
}
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- content/page.pdc --
|
|
foo
|
|
`
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*pandoc" is not whitelisted in policy "security\.exec\.allow".*`)
|
|
})
|
|
|
|
c.Run("Dart SASS, OK", func(c *qt.C) {
|
|
c.Parallel()
|
|
if !dartsass.Supports() {
|
|
c.Skip()
|
|
}
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- layouts/home.html --
|
|
{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | css.Sass (dict "transpiler" "dartsass") }}
|
|
`
|
|
Test(c, files)
|
|
})
|
|
|
|
c.Run("Dart SASS, denied", func(c *qt.C) {
|
|
c.Parallel()
|
|
if !dartsass.Supports() {
|
|
c.Skip()
|
|
}
|
|
files := `
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
[security]
|
|
[security.exec]
|
|
allow="none"
|
|
-- layouts/home.html --
|
|
{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | css.Sass (dict "transpiler" "dartsass") }}
|
|
`
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*sass(-embedded)?" is not whitelisted in policy "security\.exec\.allow".*`)
|
|
})
|
|
|
|
c.Run("resources.GetRemote, OK", func(c *qt.C) {
|
|
c.Parallel()
|
|
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
|
|
c.Cleanup(func() {
|
|
ts.Close()
|
|
})
|
|
files := fmt.Sprintf(`
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
[security]
|
|
[security.http]
|
|
urls = ['.*']
|
|
-- layouts/home.html --
|
|
{{ $json := resources.GetRemote "%s/fruits.json" }}{{ $json.Content }}
|
|
`, ts.URL)
|
|
Test(c, files)
|
|
})
|
|
|
|
c.Run("resources.GetRemote, denied method", func(c *qt.C) {
|
|
c.Parallel()
|
|
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
|
|
c.Cleanup(func() {
|
|
ts.Close()
|
|
})
|
|
files := fmt.Sprintf(`
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
[security]
|
|
[security.http]
|
|
urls = ['.*']
|
|
-- layouts/home.html --
|
|
{{ $json := resources.GetRemote "%s/fruits.json" (dict "method" "DELETE" ) }}{{ $json.Content }}
|
|
`, ts.URL)
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*"DELETE" is not whitelisted in policy "security\.http\.method".*`)
|
|
})
|
|
|
|
c.Run("resources.GetRemote, denied URL", func(c *qt.C) {
|
|
c.Parallel()
|
|
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
|
|
c.Cleanup(func() {
|
|
ts.Close()
|
|
})
|
|
files := fmt.Sprintf(`
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
[security]
|
|
[security.http]
|
|
urls="none"
|
|
-- layouts/home.html --
|
|
{{ $json := resources.GetRemote "%s/fruits.json" }}{{ $json.Content }}
|
|
`, ts.URL)
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*is not whitelisted in policy "security\.http\.urls".*`)
|
|
})
|
|
|
|
c.Run("resources.GetRemote, denied loopback URL by default", func(c *qt.C) {
|
|
c.Parallel()
|
|
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
|
|
c.Cleanup(func() {
|
|
ts.Close()
|
|
})
|
|
files := fmt.Sprintf(`
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
-- layouts/home.html --
|
|
{{ $json := resources.GetRemote "%s/fruits.json" }}{{ $json.Content }}
|
|
`, ts.URL)
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*is not whitelisted in policy "security\.http\.urls".*`)
|
|
})
|
|
|
|
c.Run("resources.GetRemote, fake JSON", func(c *qt.C) {
|
|
c.Parallel()
|
|
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
|
|
c.Cleanup(func() {
|
|
ts.Close()
|
|
})
|
|
files := fmt.Sprintf(`
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
[security]
|
|
[security.http]
|
|
urls = ['.*']
|
|
-- layouts/home.html --
|
|
{{ $json := resources.GetRemote "%s/fakejson.json" }}{{ $json.Content }}
|
|
`, ts.URL)
|
|
_, err := TestE(c, files)
|
|
c.Assert(err, qt.IsNotNil)
|
|
c.Assert(err, qt.ErrorMatches, `(?s).*failed to resolve media type.*`)
|
|
})
|
|
|
|
c.Run("resources.GetRemote, fake JSON whitelisted", func(c *qt.C) {
|
|
c.Parallel()
|
|
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
|
|
c.Cleanup(func() {
|
|
ts.Close()
|
|
})
|
|
files := fmt.Sprintf(`
|
|
-- hugo.toml --
|
|
baseURL = "https://example.org"
|
|
[security]
|
|
[security.http]
|
|
urls = ['.*']
|
|
mediaTypes=["application/json"]
|
|
-- layouts/home.html --
|
|
{{ $json := resources.GetRemote "%s/fakejson.json" }}{{ $json.Content }}
|
|
`, ts.URL)
|
|
Test(c, files)
|
|
})
|
|
}
|