/
githubmirror
/
hugo
Обзор
Документация
Войти
/
githubmirror
/
hugo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
config/security/securityConfig.go
393 строки
11 KB
Bjørn Erik Pedersen
Remove tailwindcss from the default security.exec.allow list (note)
10 авг 2026, 16:59
10 авг 2026, 16:59
8a55df7
Код
Авторство
О чём код?
// 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" "net/netip" "net/url" "reflect" "slices" "strconv" "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: MustNewWhitelist( "^(dart-)?sass$", // sass, dart-sass "^go$", // for Go Modules "^git$", // For Git info "^node$", // Used as the runtime for Node tools. "^postcss$", ), // These have been tested to work with Hugo's external programs // on Windows, Linux and MacOS. OsEnv: MustNewWhitelist(`(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$`), }, Funcs: Funcs{ Getenv: MustNewWhitelist("^HUGO_", "^CI$"), }, HTTP: HTTP{ // Allow URLs whose host starts with a letter (the typical // "https://example.com" shape), deny anything that looks like // localhost, and deny URLs with userinfo ("http://user@...") to // foil the obvious SSRF bypass. Public IP literals are collateral // blocks; users who need them can override security.http.urls. URLs: MustNewWhitelist( `(?i)^https?://[a-z0-9]`, `! ^https?://\d+\.`, `! (?i)localhost`, `! (?i)^https?://[^/?#]*@`, ), Methods: MustNewWhitelist("(?i)GET|POST"), }, Node: Node{ Permissions: NodePermissions{ Disable: false, AllowRead: []string{"."}, AllowWrite: []string{}, // No write access by default. AllowAddons: []string{"tailwindcss"}, // tailwindcss does not work without addon permissions. AllowWorker: []string{"tailwindcss"}, // tailwindcss needs worker access. AllowChildProcess: []string{"tailwindcss"}, // detect-libc spawns getconf on some Linux setups. }, }, // Content under /content is treated as untrusted. text/html bodies are // emitted verbatim and are an XSS sink, so they are denied by default. // Everything else is allowed because Whitelist treats a deny-only list as // "allow anything not denied". AllowContent: MustNewWhitelist("! ^text/html$"), } // Config is the top level security config. // <docsmeta>{"name": "security", "description": "This section holds the top level security config.", "newIn": "0.91.0" }</docsmeta> type Config struct { // Restricts access to os.Exec.... // <docsmeta>{ "newIn": "0.91.0" }</docsmeta> Exec Exec `json:"exec"` // Restricts access to certain template funcs. Funcs Funcs `json:"funcs"` // Restricts access to resources.GetRemote, getJSON, getCSV. HTTP HTTP `json:"http"` // Node holds Node.js security settings. Node Node `json:"node"` // AllowContent restricts which content media types may be used for // pages under /content. Matched against the full MIME type (e.g. // "text/html"). text/html is denied by default because Hugo emits the // body verbatim. AllowContent Whitelist `json:"allowContent"` // 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"` // Media types where the Content-Type in the response is used instead of resolving from the file content. MediaTypes Whitelist `json:"mediaTypes"` } // Node holds Node.js security settings. type Node struct { // Permissions configures Node's --permission flag for file system access control. Permissions NodePermissions `json:"permissions"` } // NodePermissions configures the Node.js permission model (--permission). // Paths are relative to the working directory; "." means the working directory itself. // Use "*" to allow all paths. type NodePermissions struct { // Disable turns off the Node.js permission model entirely. Disable bool `json:"disable"` AllowRead []string `json:"allowRead"` AllowWrite []string `json:"allowWrite"` AllowAddons []string `json:"allowAddons"` AllowWorker []string `json:"allowWorker"` AllowChildProcess []string `json:"allowChildProcess"` } // IsEnabled reports whether the Node.js permission model is active. func (p NodePermissions) IsEnabled() bool { return !p.Disable } // 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(u string) error { deny := func(name string) error { return &AccessDeniedError{ name: name, path: "security.http.urls", policies: c.ToTOML(), } } if !c.HTTP.URLs.Accept(u) { return deny(u) } // A host can be written as an integer/hex/octal IPv4 literal // (e.g. http://2130706433/ == http://127.0.0.1/) that has no dot and // thus slips past IP-literal deny rules. Re-check the canonical form so // the policy treats every encoding of the same address alike. if canon, ok := canonicalIPv4URL(u); ok && !c.HTTP.URLs.Accept(canon) { return deny(u) } return nil } // canonicalIPv4URL rewrites an integer/hex/octal IPv4 host in rawURL to its // canonical dotted-decimal form (inet_aton semantics), returning ok=false when // the host is a normal name or already dotted-decimal. func canonicalIPv4URL(rawURL string) (string, bool) { u, err := url.Parse(rawURL) if err != nil { return "", false } host := u.Hostname() ip, ok := parseInetAtonIPv4(host) if !ok || ip.String() == host { return "", false } if port := u.Port(); port != "" { u.Host = ip.String() + ":" + port } else { u.Host = ip.String() } return u.String(), true } // parseInetAtonIPv4 parses the inet_aton IPv4 forms (1–4 dot-separated parts, // each decimal, octal "0..." or hex "0x..."), e.g. "2130706433", "0x7f.0.0.1". func parseInetAtonIPv4(host string) (netip.Addr, bool) { if host == "" { return netip.Addr{}, false } parts := strings.Split(host, ".") if len(parts) > 4 { return netip.Addr{}, false } vals := make([]uint64, len(parts)) for i, p := range parts { v, ok := parseCInt(p) if !ok { return netip.Addr{}, false } vals[i] = v } maxLast := []uint64{0xffffffff, 0xffffff, 0xffff, 0xff}[len(parts)-1] var n uint64 for i, v := range vals { if i == len(parts)-1 { if v > maxLast { return netip.Addr{}, false } n |= v } else { if v > 0xff { return netip.Addr{}, false } n |= v << (8 * (3 - i)) } } return netip.AddrFrom4([4]byte{byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n)}), true } func parseCInt(s string) (uint64, bool) { base := 10 switch { case len(s) >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X'): base, s = 16, s[2:] case len(s) >= 2 && s[0] == '0': base, s = 8, s[1:] } if s == "" { return 0, false } v, err := strconv.ParseUint(s, base, 64) if err != nil { return 0, false } return v, true } 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 } func (c Config) CheckAllowedContent(mediaType string) error { if !c.AllowContent.Accept(mediaType) { return &AccessDeniedError{ name: mediaType, path: "security.allowContent", policies: c.ToTOML(), } } return nil } // ToSecurityMap converts c to a map with 'security' as the root key. func (c Config) ToSecurityMap() map[string]any { // Take it to JSON and back to get proper casing etc. asJson, err := json.Marshal(c) herrors.Must(err) m := make(map[string]any) herrors.Must(json.Unmarshal(asJson, &m)) // Add the root sec := map[string]any{ "security": m, } return sec } // DecodeConfig creates a privacy Config from a given Hugo configuration. func DecodeConfig(cfg config.Provider) (Config, error) { sc := DefaultConfig // Deep copy slices to prevent mapstructure from mutating DefaultConfig. sc.Node.Permissions.AllowRead = slices.Clone(sc.Node.Permissions.AllowRead) sc.Node.Permissions.AllowWrite = slices.Clone(sc.Node.Permissions.AllowWrite) sc.Node.Permissions.AllowAddons = slices.Clone(sc.Node.Permissions.AllowAddons) sc.Node.Permissions.AllowWorker = slices.Clone(sc.Node.Permissions.AllowWorker) sc.Node.Permissions.AllowChildProcess = slices.Clone(sc.Node.Permissions.AllowChildProcess) 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 any, ) (any, error) { if t != reflect.TypeFor[Whitelist]() { return data, nil } wl := types.ToStringSlicePreserveString(data) return NewWhitelist(wl...) } } // 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) }