diff options
Diffstat (limited to 'config/security/securityConfig.go')
-rw-r--r-- | config/security/securityConfig.go | 227 |
1 files changed, 227 insertions, 0 deletions
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:"enableInlineShortcodes"` +} + +// Exec holds os/exec policies. +type Exec struct { + Allow Whitelist `json:"allow"` + OsEnv Whitelist `json:"osEnv"` +} + +// Funcs holds template funcs policies. +type Funcs struct { + // OS env keys allowed to query in os.Getenv. + Getenv Whitelist `json:"getenv"` +} + +type HTTP struct { + // URLs to allow in remote HTTP (resources.Get, getJSON, getCSV). + URLs Whitelist `json:"urls"` + + // HTTP methods to allow. + Methods Whitelist `json:"methods"` +} + +// ToTOML converts c to TOML with [security] as the root. +func (c Config) ToTOML() string { + sec := c.ToSecurityMap() + + var b bytes.Buffer + + if err := parser.InterfaceToConfig(sec, metadecoders.TOML, &b); err != nil { + panic(err) + } + + return strings.TrimSpace(b.String()) +} + +func (c Config) CheckAllowedExec(name string) error { + if !c.Exec.Allow.Accept(name) { + return &AccessDeniedError{ + name: name, + path: "security.exec.allow", + policies: c.ToTOML(), + } + } + return nil + +} + +func (c Config) CheckAllowedGetEnv(name string) error { + if !c.Funcs.Getenv.Accept(name) { + return &AccessDeniedError{ + name: name, + path: "security.funcs.getenv", + policies: c.ToTOML(), + } + } + return nil +} + +func (c Config) CheckAllowedHTTPURL(url string) error { + if !c.HTTP.URLs.Accept(url) { + return &AccessDeniedError{ + name: url, + path: "security.http.urls", + policies: c.ToTOML(), + } + } + return nil +} + +func (c Config) CheckAllowedHTTPMethod(method string) error { + if !c.HTTP.Methods.Accept(method) { + return &AccessDeniedError{ + name: method, + path: "security.http.method", + policies: c.ToTOML(), + } + } + return nil +} + +// ToSecurityMap converts c to a map with 'security' as the root key. +func (c Config) ToSecurityMap() map[string]interface{} { + // Take it to JSON and back to get proper casing etc. + asJson, err := json.Marshal(c) + herrors.Must(err) + m := make(map[string]interface{}) + herrors.Must(json.Unmarshal(asJson, &m)) + + // Add the root + sec := map[string]interface{}{ + "security": m, + } + return sec + +} + +// DecodeConfig creates a privacy Config from a given Hugo configuration. +func DecodeConfig(cfg config.Provider) (Config, error) { + sc := DefaultConfig + if cfg.IsSet(securityConfigKey) { + m := cfg.GetStringMap(securityConfigKey) + dec, err := mapstructure.NewDecoder( + &mapstructure.DecoderConfig{ + WeaklyTypedInput: true, + Result: &sc, + DecodeHook: stringSliceToWhitelistHook(), + }, + ) + if err != nil { + return sc, err + } + + if err = dec.Decode(m); err != nil { + return sc, err + } + } + + if !sc.EnableInlineShortcodes { + // Legacy + sc.EnableInlineShortcodes = cfg.GetBool("enableInlineShortcodes") + } + + return sc, nil + +} + +func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + + if t != reflect.TypeOf(Whitelist{}) { + return data, nil + } + + wl := types.ToStringSlicePreserveString(data) + + return NewWhitelist(wl...), nil + + } +} + +// AccessDeniedError represents a security policy conflict. +type AccessDeniedError struct { + path string + name string + policies string +} + +func (e *AccessDeniedError) Error() string { + return fmt.Sprintf("access denied: %q is not whitelisted in policy %q; the current security configuration is:\n\n%s\n\n", e.name, e.path, e.policies) +} + +// IsAccessDenied reports whether err is an AccessDeniedError +func IsAccessDenied(err error) bool { + var notFoundErr *AccessDeniedError + return errors.As(err, ¬FoundErr) +} |