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 }