securityConfig.go (5392B)
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)^(PATH|PATHEXT|APPDATA|TMP|TEMP|TERM)$"), 46 }, 47 Funcs: Funcs{ 48 Getenv: NewWhitelist("^HUGO_"), 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, ¬FoundErr) 227 }