github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/config/security/securityConfig.go (about)

     1  // Copyright 2018 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package security
    15  
    16  import (
    17  	"bytes"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  
    24  	"github.com/gohugoio/hugo/common/herrors"
    25  	"github.com/gohugoio/hugo/common/types"
    26  	"github.com/gohugoio/hugo/config"
    27  	"github.com/gohugoio/hugo/parser"
    28  	"github.com/gohugoio/hugo/parser/metadecoders"
    29  	"github.com/mitchellh/mapstructure"
    30  )
    31  
    32  const securityConfigKey = "security"
    33  
    34  // DefaultConfig holds the default security policy.
    35  var DefaultConfig = Config{
    36  	Exec: Exec{
    37  		Allow: NewWhitelist(
    38  			"^dart-sass-embedded$",
    39  			"^go$",  // for Go Modules
    40  			"^npx$", // used by all Node tools (Babel, PostCSS).
    41  			"^postcss$",
    42  		),
    43  		// These have been tested to work with Hugo's external programs
    44  		// on Windows, Linux and MacOS.
    45  		OsEnv: NewWhitelist(`(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+)$`),
    46  	},
    47  	Funcs: Funcs{
    48  		Getenv: NewWhitelist("^HUGO_", "^CI$"),
    49  	},
    50  	HTTP: HTTP{
    51  		URLs:    NewWhitelist(".*"),
    52  		Methods: NewWhitelist("(?i)GET|POST"),
    53  	},
    54  }
    55  
    56  // Config is the top level security config.
    57  type Config struct {
    58  	// Restricts access to os.Exec.
    59  	Exec Exec `json:"exec"`
    60  
    61  	// Restricts access to certain template funcs.
    62  	Funcs Funcs `json:"funcs"`
    63  
    64  	// Restricts access to resources.Get, getJSON, getCSV.
    65  	HTTP HTTP `json:"http"`
    66  
    67  	// Allow inline shortcodes
    68  	EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
    69  }
    70  
    71  // Exec holds os/exec policies.
    72  type Exec struct {
    73  	Allow Whitelist `json:"allow"`
    74  	OsEnv Whitelist `json:"osEnv"`
    75  }
    76  
    77  // Funcs holds template funcs policies.
    78  type Funcs struct {
    79  	// OS env keys allowed to query in os.Getenv.
    80  	Getenv Whitelist `json:"getenv"`
    81  }
    82  
    83  type HTTP struct {
    84  	// URLs to allow in remote HTTP (resources.Get, getJSON, getCSV).
    85  	URLs Whitelist `json:"urls"`
    86  
    87  	// HTTP methods to allow.
    88  	Methods Whitelist `json:"methods"`
    89  }
    90  
    91  // ToTOML converts c to TOML with [security] as the root.
    92  func (c Config) ToTOML() string {
    93  	sec := c.ToSecurityMap()
    94  
    95  	var b bytes.Buffer
    96  
    97  	if err := parser.InterfaceToConfig(sec, metadecoders.TOML, &b); err != nil {
    98  		panic(err)
    99  	}
   100  
   101  	return strings.TrimSpace(b.String())
   102  }
   103  
   104  func (c Config) CheckAllowedExec(name string) error {
   105  	if !c.Exec.Allow.Accept(name) {
   106  		return &AccessDeniedError{
   107  			name:     name,
   108  			path:     "security.exec.allow",
   109  			policies: c.ToTOML(),
   110  		}
   111  	}
   112  	return nil
   113  
   114  }
   115  
   116  func (c Config) CheckAllowedGetEnv(name string) error {
   117  	if !c.Funcs.Getenv.Accept(name) {
   118  		return &AccessDeniedError{
   119  			name:     name,
   120  			path:     "security.funcs.getenv",
   121  			policies: c.ToTOML(),
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  func (c Config) CheckAllowedHTTPURL(url string) error {
   128  	if !c.HTTP.URLs.Accept(url) {
   129  		return &AccessDeniedError{
   130  			name:     url,
   131  			path:     "security.http.urls",
   132  			policies: c.ToTOML(),
   133  		}
   134  	}
   135  	return nil
   136  }
   137  
   138  func (c Config) CheckAllowedHTTPMethod(method string) error {
   139  	if !c.HTTP.Methods.Accept(method) {
   140  		return &AccessDeniedError{
   141  			name:     method,
   142  			path:     "security.http.method",
   143  			policies: c.ToTOML(),
   144  		}
   145  	}
   146  	return nil
   147  }
   148  
   149  // ToSecurityMap converts c to a map with 'security' as the root key.
   150  func (c Config) ToSecurityMap() map[string]any {
   151  	// Take it to JSON and back to get proper casing etc.
   152  	asJson, err := json.Marshal(c)
   153  	herrors.Must(err)
   154  	m := make(map[string]any)
   155  	herrors.Must(json.Unmarshal(asJson, &m))
   156  
   157  	// Add the root
   158  	sec := map[string]any{
   159  		"security": m,
   160  	}
   161  	return sec
   162  
   163  }
   164  
   165  // DecodeConfig creates a privacy Config from a given Hugo configuration.
   166  func DecodeConfig(cfg config.Provider) (Config, error) {
   167  	sc := DefaultConfig
   168  	if cfg.IsSet(securityConfigKey) {
   169  		m := cfg.GetStringMap(securityConfigKey)
   170  		dec, err := mapstructure.NewDecoder(
   171  			&mapstructure.DecoderConfig{
   172  				WeaklyTypedInput: true,
   173  				Result:           &sc,
   174  				DecodeHook:       stringSliceToWhitelistHook(),
   175  			},
   176  		)
   177  		if err != nil {
   178  			return sc, err
   179  		}
   180  
   181  		if err = dec.Decode(m); err != nil {
   182  			return sc, err
   183  		}
   184  	}
   185  
   186  	if !sc.EnableInlineShortcodes {
   187  		// Legacy
   188  		sc.EnableInlineShortcodes = cfg.GetBool("enableInlineShortcodes")
   189  	}
   190  
   191  	return sc, nil
   192  
   193  }
   194  
   195  func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
   196  	return func(
   197  		f reflect.Type,
   198  		t reflect.Type,
   199  		data any) (any, error) {
   200  
   201  		if t != reflect.TypeOf(Whitelist{}) {
   202  			return data, nil
   203  		}
   204  
   205  		wl := types.ToStringSlicePreserveString(data)
   206  
   207  		return NewWhitelist(wl...), nil
   208  
   209  	}
   210  }
   211  
   212  // AccessDeniedError represents a security policy conflict.
   213  type AccessDeniedError struct {
   214  	path     string
   215  	name     string
   216  	policies string
   217  }
   218  
   219  func (e *AccessDeniedError) Error() string {
   220  	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)
   221  }
   222  
   223  // IsAccessDenied reports whether err is an AccessDeniedError
   224  func IsAccessDenied(err error) bool {
   225  	var notFoundErr *AccessDeniedError
   226  	return errors.As(err, &notFoundErr)
   227  }