summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--common/collections/slice.go10
-rw-r--r--common/herrors/errors.go7
-rw-r--r--common/hexec/exec.go276
-rw-r--r--common/hexec/safeCommand.go45
-rw-r--r--common/hugo/hugo.go19
-rw-r--r--config/defaultConfigProvider.go2
-rw-r--r--config/security/docshelper.go26
-rw-r--r--config/security/securityConfig.go227
-rw-r--r--config/security/securityonfig_test.go166
-rw-r--r--config/security/whitelist.go102
-rw-r--r--config/security/whitelist_test.go47
-rw-r--r--create/content.go14
-rw-r--r--deps/deps.go19
-rw-r--r--docs/config/_default/security.toml13
-rw-r--r--docs/content/en/about/security-model/index.md21
-rw-r--r--docs/content/en/getting-started/configuration.md4
-rw-r--r--docs/data/docs.json37
-rw-r--r--helpers/content.go4
-rw-r--r--helpers/content_test.go2
-rw-r--r--helpers/general_test.go2
-rw-r--r--helpers/testhelpers_test.go2
-rw-r--r--htesting/test_helpers.go2
-rw-r--r--hugolib/config.go9
-rw-r--r--hugolib/js_test.go10
-rw-r--r--hugolib/page_test.go73
-rw-r--r--hugolib/resource_chain_babel_test.go14
-rw-r--r--hugolib/resource_chain_test.go8
-rw-r--r--hugolib/securitypolicies_test.go202
-rw-r--r--hugolib/shortcode.go4
-rw-r--r--hugolib/shortcode_test.go6
-rw-r--r--hugolib/site.go42
-rw-r--r--hugolib/testdata/cities.csv130
-rw-r--r--hugolib/testdata/fruits.json5
-rw-r--r--hugolib/testhelpers_test.go12
-rw-r--r--markup/asciidocext/convert.go36
-rw-r--r--markup/asciidocext/convert_test.go50
-rw-r--r--markup/converter/converter.go2
-rw-r--r--markup/internal/external.go54
-rw-r--r--markup/pandoc/convert.go36
-rw-r--r--markup/pandoc/convert_test.go6
-rw-r--r--markup/rst/convert.go58
-rw-r--r--markup/rst/convert_test.go11
-rw-r--r--modules/client.go29
-rw-r--r--modules/client_test.go4
-rw-r--r--resources/resource_factories/create/create.go16
-rw-r--r--resources/resource_spec.go5
-rw-r--r--resources/resource_transformers/babel/babel.go46
-rw-r--r--resources/resource_transformers/htesting/testhelpers.go2
-rw-r--r--resources/resource_transformers/postcss/postcss.go47
-rw-r--r--resources/resource_transformers/tocss/dartsass/client.go19
-rw-r--r--resources/resource_transformers/tocss/dartsass/transform.go13
-rw-r--r--resources/testhelpers_test.go4
-rw-r--r--tpl/collections/collections_test.go3
-rw-r--r--tpl/data/data.go7
-rw-r--r--tpl/data/resources.go7
-rw-r--r--tpl/data/resources_test.go7
-rw-r--r--tpl/os/os.go4
-rw-r--r--tpl/transform/transform_test.go2
58 files changed, 1701 insertions, 329 deletions
diff --git a/common/collections/slice.go b/common/collections/slice.go
index 38ca86b08..07ad48eb3 100644
--- a/common/collections/slice.go
+++ b/common/collections/slice.go
@@ -64,3 +64,13 @@ func Slice(args ...interface{}) interface{} {
}
return slice.Interface()
}
+
+// StringSliceToInterfaceSlice converts ss to []interface{}.
+func StringSliceToInterfaceSlice(ss []string) []interface{} {
+ result := make([]interface{}, len(ss))
+ for i, s := range ss {
+ result[i] = s
+ }
+ return result
+
+}
diff --git a/common/herrors/errors.go b/common/herrors/errors.go
index fded30b1a..00aed1eb6 100644
--- a/common/herrors/errors.go
+++ b/common/herrors/errors.go
@@ -88,3 +88,10 @@ func GetGID() uint64 {
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
// and this error is used to signal those situations.
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
+
+// Must panics if err != nil.
+func Must(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/common/hexec/exec.go b/common/hexec/exec.go
new file mode 100644
index 000000000..a8bdd1bb7
--- /dev/null
+++ b/common/hexec/exec.go
@@ -0,0 +1,276 @@
+// Copyright 2020 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 hexec
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+
+ "os"
+ "os/exec"
+
+ "github.com/cli/safeexec"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/config/security"
+)
+
+var WithDir = func(dir string) func(c *commandeer) {
+ return func(c *commandeer) {
+ c.dir = dir
+ }
+}
+
+var WithContext = func(ctx context.Context) func(c *commandeer) {
+ return func(c *commandeer) {
+ c.ctx = ctx
+ }
+}
+
+var WithStdout = func(w io.Writer) func(c *commandeer) {
+ return func(c *commandeer) {
+ c.stdout = w
+ }
+}
+
+var WithStderr = func(w io.Writer) func(c *commandeer) {
+ return func(c *commandeer) {
+ c.stderr = w
+ }
+}
+
+var WithStdin = func(r io.Reader) func(c *commandeer) {
+ return func(c *commandeer) {
+ c.stdin = r
+ }
+}
+
+var WithEnviron = func(env []string) func(c *commandeer) {
+ return func(c *commandeer) {
+ setOrAppend := func(s string) {
+ k1, _ := config.SplitEnvVar(s)
+ var found bool
+ for i, v := range c.env {
+ k2, _ := config.SplitEnvVar(v)
+ if k1 == k2 {
+ found = true
+ c.env[i] = s
+ }
+ }
+
+ if !found {
+ c.env = append(c.env, s)
+ }
+ }
+
+ for _, s := range env {
+ setOrAppend(s)
+ }
+ }
+}
+
+// New creates a new Exec using the provided security config.
+func New(cfg security.Config) *Exec {
+ var baseEnviron []string
+ for _, v := range os.Environ() {
+ k, _ := config.SplitEnvVar(v)
+ if cfg.Exec.OsEnv.Accept(k) {
+ baseEnviron = append(baseEnviron, v)
+ }
+ }
+
+ return &Exec{
+ sc: cfg,
+ baseEnviron: baseEnviron,
+ }
+}
+
+// IsNotFound reports whether this is an error about a binary not found.
+func IsNotFound(err error) bool {
+ var notFoundErr *NotFoundError
+ return errors.As(err, &notFoundErr)
+}
+
+// SafeCommand is a wrapper around os/exec Command which uses a LookPath
+// implementation that does not search in current directory before looking in PATH.
+// See https://github.com/cli/safeexec and the linked issues.
+func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
+ bin, err := safeexec.LookPath(name)
+ if err != nil {
+ return nil, err
+ }
+
+ return exec.Command(bin, arg...), nil
+}
+
+// Exec encorces a security policy for commands run via os/exec.
+type Exec struct {
+ sc security.Config
+
+ // os.Environ filtered by the Exec.OsEnviron whitelist filter.
+ baseEnviron []string
+}
+
+// New will fail if name is not allowed according to the configured security policy.
+// Else a configured Runner will be returned ready to be Run.
+func (e *Exec) New(name string, arg ...interface{}) (Runner, error) {
+ if err := e.sc.CheckAllowedExec(name); err != nil {
+ return nil, err
+ }
+
+ env := make([]string, len(e.baseEnviron))
+ copy(env, e.baseEnviron)
+
+ cm := &commandeer{
+ name: name,
+ env: env,
+ }
+
+ return cm.command(arg...)
+
+}
+
+// Npx is a convenience method to create a Runner running npx --no-install <name> <args.
+func (e *Exec) Npx(name string, arg ...interface{}) (Runner, error) {
+ arg = append(arg[:0], append([]interface{}{"--no-install", name}, arg[0:]...)...)
+ return e.New("npx", arg...)
+}
+
+// Sec returns the security policies this Exec is configured with.
+func (e *Exec) Sec() security.Config {
+ return e.sc
+}
+
+type NotFoundError struct {
+ name string
+}
+
+func (e *NotFoundError) Error() string {
+ return fmt.Sprintf("binary with name %q not found", e.name)
+}
+
+// Runner wraps a *os.Cmd.
+type Runner interface {
+ Run() error
+ StdinPipe() (io.WriteCloser, error)
+}
+
+type cmdWrapper struct {
+ name string
+ c *exec.Cmd
+
+ outerr *bytes.Buffer
+}
+
+var notFoundRe = regexp.MustCompile(`(?s)not found:|could not determine executable`)
+
+func (c *cmdWrapper) Run() error {
+ err := c.c.Run()
+ if err == nil {
+ return nil
+ }
+ if notFoundRe.MatchString(c.outerr.String()) {
+ return &NotFoundError{name: c.name}
+ }
+ return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
+}
+
+func (c *cmdWrapper) StdinPipe() (io.WriteCloser, error) {
+ return c.c.StdinPipe()
+}
+
+type commandeer struct {
+ stdout io.Writer
+ stderr io.Writer
+ stdin io.Reader
+ dir string
+ ctx context.Context
+
+ name string
+ env []string
+}
+
+func (c *commandeer) command(arg ...interface{}) (*cmdWrapper, error) {
+ if c == nil {
+ return nil, nil
+ }
+
+ var args []string
+ for _, a := range arg {
+ switch v := a.(type) {
+ case string:
+ args = append(args, v)
+ case func(*commandeer):
+ v(c)
+ default:
+ return nil, fmt.Errorf("invalid argument to command: %T", a)
+ }
+ }
+
+ bin, err := safeexec.LookPath(c.name)
+ if err != nil {
+ return nil, &NotFoundError{
+ name: c.name,
+ }
+ }
+
+ outerr := &bytes.Buffer{}
+ if c.stderr == nil {
+ c.stderr = outerr
+ } else {
+ c.stderr = io.MultiWriter(c.stderr, outerr)
+ }
+
+ var cmd *exec.Cmd
+
+ if c.ctx != nil {
+ cmd = exec.CommandContext(c.ctx, bin, args...)
+ } else {
+ cmd = exec.Command(bin, args...)
+ }
+
+ cmd.Stdin = c.stdin
+ cmd.Stderr = c.stderr
+ cmd.Stdout = c.stdout
+ cmd.Env = c.env
+ cmd.Dir = c.dir
+
+ return &cmdWrapper{outerr: outerr, c: cmd, name: c.name}, nil
+}
+
+// InPath reports whether binaryName is in $PATH.
+func InPath(binaryName string) bool {
+ if strings.Contains(binaryName, "/") {
+ panic("binary name should not contain any slash")
+ }
+ _, err := safeexec.LookPath(binaryName)
+ return err == nil
+}
+
+// LookPath finds the path to binaryName in $PATH.
+// Returns "" if not found.
+func LookPath(binaryName string) string {
+ if strings.Contains(binaryName, "/") {
+ panic("binary name should not contain any slash")
+ }
+ s, err := safeexec.LookPath(binaryName)
+ if err != nil {
+ return ""
+ }
+ return s
+}
diff --git a/common/hexec/safeCommand.go b/common/hexec/safeCommand.go
deleted file mode 100644
index 6d5c73982..000000000
--- a/common/hexec/safeCommand.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2020 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 hexec
-
-import (
- "context"
-
- "os/exec"
-
- "github.com/cli/safeexec"
-)
-
-// SafeCommand is a wrapper around os/exec Command which uses a LookPath
-// implementation that does not search in current directory before looking in PATH.
-// See https://github.com/cli/safeexec and the linked issues.
-func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
- bin, err := safeexec.LookPath(name)
- if err != nil {
- return nil, err
- }
-
- return exec.Command(bin, arg...), nil
-}
-
-// SafeCommandContext wraps CommandContext
-// See SafeCommand for more context.
-func SafeCommandContext(ctx context.Context, name string, arg ...string) (*exec.Cmd, error) {
- bin, err := safeexec.LookPath(name)
- if err != nil {
- return nil, err
- }
-
- return exec.CommandContext(ctx, bin, arg...), nil
-}
diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go
index 4548de93a..d8f92e298 100644
--- a/common/hugo/hugo.go
+++ b/common/hugo/hugo.go
@@ -89,8 +89,10 @@ func NewInfo(environment string) Info {
}
}
+// GetExecEnviron creates and gets the common os/exec environment used in the
+// external programs we interact with via os/exec, e.g. postcss.
func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
- env := os.Environ()
+ var env []string
nodepath := filepath.Join(workDir, "node_modules")
if np := os.Getenv("NODE_PATH"); np != "" {
nodepath = workDir + string(os.PathListSeparator) + np
@@ -98,12 +100,15 @@ func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
config.SetEnvVars(&env, "NODE_PATH", nodepath)
config.SetEnvVars(&env, "PWD", workDir)
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
- fis, err := afero.ReadDir(fs, files.FolderJSConfig)
- if err == nil {
- for _, fi := range fis {
- key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
- value := fi.(hugofs.FileMetaInfo).Meta().Filename
- config.SetEnvVars(&env, key, value)
+
+ if fs != nil {
+ fis, err := afero.ReadDir(fs, files.FolderJSConfig)
+ if err == nil {
+ for _, fi := range fis {
+ key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
+ value := fi.(hugofs.FileMetaInfo).Meta().Filename
+ config.SetEnvVars(&env, key, value)
+ }
}
}
diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go
index 0a10d5cc6..7701e765a 100644
--- a/config/defaultConfigProvider.go
+++ b/config/defaultConfigProvider.go
@@ -44,6 +44,8 @@ var (
"permalinks": true,
"related": true,
"sitemap": true,
+ "privacy": true,
+ "security": true,
"taxonomies": true,
}
diff --git a/config/security/docshelper.go b/config/security/docshelper.go
new file mode 100644
index 000000000..ade03560e
--- /dev/null
+++ b/config/security/docshelper.go
@@ -0,0 +1,26 @@
+// Copyright 2021 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 security
+
+import (
+ "github.com/gohugoio/hugo/docshelper"
+)
+
+func init() {
+ docsProvider := func() docshelper.DocProvider {
+
+ return docshelper.DocProvider{"config": DefaultConfig.ToSecurityMap()}
+ }
+ docshelper.AddDocProviderFunc(docsProvider)
+}
diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go
new file mode 100644
index 000000000..09c5cb625
--- /dev/null
+++ b/config/security/securityConfig.go
@@ -0,0 +1,227 @@
+// Copyright 2018 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 security
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/parser"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/mitchellh/mapstructure"
+)
+
+const securityConfigKey = "security"
+
+// DefaultConfig holds the default security policy.
+var DefaultConfig = Config{
+ Exec: Exec{
+ Allow: NewWhitelist(
+ "^dart-sass-embedded$",
+ "^go$", // for Go Modules
+ "^npx$", // used by all Node tools (Babel, PostCSS).
+ "^postcss$",
+ ),
+ // These have been tested to work with Hugo's external programs
+ // on Windows, Linux and MacOS.
+ OsEnv: NewWhitelist("(?i)^(PATH|PATHEXT|APPDATA|TMP|TEMP|TERM)$"),
+ },
+ Funcs: Funcs{
+ Getenv: NewWhitelist("^HUGO_"),
+ },
+ HTTP: HTTP{
+ URLs: NewWhitelist(".*"),
+ Methods: NewWhitelist("(?i)GET|POST"),
+ },
+}
+
+// Config is the top level security config.
+type Config struct {
+ // Restricts access to os.Exec.
+ Exec Exec `json:"exec"`
+
+ // Restricts access to certain template funcs.
+ Funcs Funcs `json:"funcs"`
+
+ // Restricts access to resources.Get, getJSON, getCSV.
+ HTTP HTTP `json:"http"`
+
+ // Allow inline shortcodes
+ EnableInlineShortcodes bool `json:"enable