diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-12-04 12:07:54 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-04 12:07:54 +0100 |
commit | 9f978d387f8b7cb6bc03fe6b4dd52bb16862a784 (patch) | |
tree | dc53e021fbf8a89e7ff0d3e86bbe9416ce5d7ecb | |
parent | 14d85ec136413dcfc96ad8e4d31633f8f9cbf410 (diff) |
Pull in the latest code from Go's template packages (#11771)
Fixes #10707
Fixes #11507
25 files changed, 417 insertions, 190 deletions
diff --git a/config/allconfig/load.go b/config/allconfig/load.go index e7dae1806..7d706c7e3 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -34,7 +34,6 @@ import ( hglob "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/tpl" "github.com/spf13/afero" ) @@ -91,9 +90,6 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { return nil, fmt.Errorf("failed to init config: %w", err) } - // This is unfortunate, but these are global settings. - tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl) - loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning) return configs, nil diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go index 16f8c23d8..e3bffedca 100644 --- a/config/security/securityConfig.go +++ b/config/security/securityConfig.go @@ -68,9 +68,6 @@ type Config struct { // Allow inline shortcodes EnableInlineShortcodes bool `json:"enableInlineShortcodes"` - - // Go templates related security config. - GoTemplates GoTemplates `json:"goTemplates"` } // Exec holds os/exec policies. @@ -96,15 +93,6 @@ type HTTP struct { MediaTypes Whitelist `json:"mediaTypes"` } -type GoTemplates struct { - - // Enable to allow template actions inside bakcticks in ES6 template literals. - // This was blocked in Hugo 0.114.0 for security reasons and you now get errors on the form - // "... appears in a JS template literal" if you have this in your templates. - // See https://github.com/golang/go/issues/59234 - AllowActionJSTmpl bool -} - // ToTOML converts c to TOML with [security] as the root. func (c Config) ToTOML() string { sec := c.ToSecurityMap() @@ -127,7 +115,6 @@ func (c Config) CheckAllowedExec(name string) error { } } return nil - } func (c Config) CheckAllowedGetEnv(name string) error { @@ -176,7 +163,6 @@ func (c Config) ToSecurityMap() map[string]any { "security": m, } return sec - } // DecodeConfig creates a privacy Config from a given Hugo configuration. @@ -206,15 +192,14 @@ func DecodeConfig(cfg config.Provider) (Config, error) { } return sc, nil - } func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType { return func( f reflect.Type, t reflect.Type, - data any) (any, error) { - + data any, + ) (any, error) { if t != reflect.TypeOf(Whitelist{}) { return data, nil } @@ -222,7 +207,6 @@ func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType { wl := types.ToStringSlicePreserveString(data) return NewWhitelist(wl...) - } } diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go index cdfbe6341..3d58288c9 100644 --- a/config/security/securityConfig_test.go +++ b/config/security/securityConfig_test.go @@ -53,7 +53,6 @@ getEnv=["a", "b"] c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Funcs.Getenv.Accept("a"), qt.IsTrue) c.Assert(pc.Funcs.Getenv.Accept("c"), qt.IsFalse) - }) c.Run("String whitelist", func(c *qt.C) { @@ -80,7 +79,6 @@ osEnv="b" c.Assert(pc.Exec.Allow.Accept("d"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("b"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) - }) c.Run("Default exec.osEnv", func(c *qt.C) { @@ -105,7 +103,6 @@ allow="a" c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) - }) c.Run("Enable inline shortcodes, legacy", func(c *qt.C) { @@ -129,9 +126,7 @@ osEnv="b" pc, err := DecodeConfig(cfg) c.Assert(err, qt.IsNil) c.Assert(pc.EnableInlineShortcodes, qt.IsTrue) - }) - } func TestToTOML(t *testing.T) { @@ -140,7 +135,7 @@ func TestToTOML(t *testing.T) { got := DefaultConfig.ToTOML() c.Assert(got, qt.Equals, - "[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.goTemplates]\n AllowActionJSTmpl = false\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", + "[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", ) } @@ -169,5 +164,4 @@ func TestDecodeConfigDefault(t *testing.T) { c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("MYSECRET"), qt.IsFalse) - } diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index 4ab89a547..8e14813ec 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -16,7 +16,7 @@ import ( ) func main() { - // The current is built with 2c1e5b05fe39fc5e6c730dd60e82946b8e67c6ba, tag: go1.21.1. + // The current is built with 446a5dcf5a3230ce9832682d8f521071d8a34a2b (go 1.22 dev. Thu Oct 5 12:20:11 2023 -0700) fmt.Println("Forking ...") defer fmt.Println("Done ...") diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go index 064b09107..fef952fa6 100644 --- a/tpl/internal/go_templates/fmtsort/sort_test.go +++ b/tpl/internal/go_templates/fmtsort/sort_test.go @@ -6,13 +6,14 @@ package fmtsort_test import ( "fmt" - "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" "math" "reflect" "sort" "strings" "testing" "unsafe" + + "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" ) var compareTests = [][]reflect.Value{ @@ -38,7 +39,7 @@ var compareTests = [][]reflect.Value{ ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]), ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}), ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}), - ct(reflect.TypeOf(any(any(0))), iFace, 1, 2, 3), + ct(reflect.TypeOf(any(0)), iFace, 1, 2, 3), } var iFace any @@ -190,12 +191,15 @@ func sprintKey(key reflect.Value) string { var ( ints [3]int chans = makeChans() + // pin runtime.Pinner ) func makeChans() []chan int { cs := []chan int{make(chan int), make(chan int), make(chan int)} // Order channels by address. See issue #49431. - // TODO: pin these pointers once pinning is available (#46787). + for i := range cs { + reflect.ValueOf(cs[i]).UnsafePointer() + } sort.Slice(cs, func(i, j int) bool { return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer()) }) diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go index 061f17ddb..3acb9bdf0 100644 --- a/tpl/internal/go_templates/htmltemplate/context.go +++ b/tpl/internal/go_templates/htmltemplate/context.go @@ -22,10 +22,15 @@ type context struct { delim delim urlPart urlPart jsCtx jsCtx - attr attr - element element - n parse.Node // for range break/continue - err *Error + // jsBraceDepth contains the current depth, for each JS template literal + // string interpolation expression, of braces we've seen. This is used to + // determine if the next } will close a JS template literal string + // interpolation expression or not. + jsBraceDepth []int + attr attr + element element + n parse.Node // for range break/continue + err *Error } func (c context) String() string { @@ -121,8 +126,8 @@ const ( stateJSDqStr // stateJSSqStr occurs inside a JavaScript single quoted string. stateJSSqStr - // stateJSBqStr occurs inside a JavaScript back quoted string. - stateJSBqStr + // stateJSTmplLit occurs inside a JavaScript back quoted string. + stateJSTmplLit // stateJSRegexp occurs inside a JavaScript regexp literal. stateJSRegexp // stateJSBlockCmt occurs inside a JavaScript /* block comment */. @@ -176,14 +181,14 @@ func isInTag(s state) bool { } // isInScriptLiteral returns true if s is one of the literal states within a -// <script> tag, and as such occurances of "<!--", "<script", and "</script" +// <script> tag, and as such occurrences of "<!--", "<script", and "</script" // need to be treated specially. func isInScriptLiteral(s state) bool { // Ignore the comment states (stateJSBlockCmt, stateJSLineCmt, // stateJSHTMLOpenCmt, stateJSHTMLCloseCmt) because their content is already // omitted from the output. switch s { - case stateJSDqStr, stateJSSqStr, stateJSBqStr, stateJSRegexp: + case stateJSDqStr, stateJSSqStr, stateJSTmplLit, stateJSRegexp: return true } return false diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go index a4450a4a9..de4b4de1e 100644 --- a/tpl/internal/go_templates/htmltemplate/error.go +++ b/tpl/internal/go_templates/htmltemplate/error.go @@ -222,6 +222,10 @@ const ( // Discussion: // Package html/template does not support actions inside of JS template // literals. + // + // Deprecated: ErrJSTemplate is no longer returned when an action is present + // in a JS template literal. Actions inside of JS template literals are now + // escaped as expected. ErrJSTemplate ) diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index 5d47a83d3..334bbce0f 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -8,8 +8,6 @@ import ( "bytes" "fmt" "html" - - //"internal/godebug" "io" "regexp" @@ -64,22 +62,23 @@ func evalArgs(args ...any) string { // funcMap maps command names to functions that render their inputs safe. var funcMap = template.FuncMap{ - "_html_template_attrescaper": attrEscaper, - "_html_template_commentescaper": commentEscaper, - "_html_template_cssescaper": cssEscaper, - "_html_template_cssvaluefilter": cssValueFilter, - "_html_template_htmlnamefilter": htmlNameFilter, - "_html_template_htmlescaper": htmlEscaper, - "_html_template_jsregexpescaper": jsRegexpEscaper, - "_html_template_jsstrescaper": jsStrEscaper, - "_html_template_jsvalescaper": jsValEscaper, - "_html_template_nospaceescaper": htmlNospaceEscaper, - "_html_template_rcdataescaper": rcdataEscaper, - "_html_template_srcsetescaper": srcsetFilterAndEscaper, - "_html_template_urlescaper": urlEscaper, - "_html_template_urlfilter": urlFilter, - "_html_template_urlnormalizer": urlNormalizer, - "_eval_args_": evalArgs, + "_html_template_attrescaper": attrEscaper, + "_html_template_commentescaper": commentEscaper, + "_html_template_cssescaper": cssEscaper, + "_html_template_cssvaluefilter": cssValueFilter, + "_html_template_htmlnamefilter": htmlNameFilter, + "_html_template_htmlescaper": htmlEscaper, + "_html_template_jsregexpescaper": jsRegexpEscaper, + "_html_template_jsstrescaper": jsStrEscaper, + "_html_template_jstmpllitescaper": jsTmplLitEscaper, + "_html_template_jsvalescaper": jsValEscaper, + "_html_template_nospaceescaper": htmlNospaceEscaper, + "_html_template_rcdataescaper": rcdataEscaper, + "_html_template_srcsetescaper": srcsetFilterAndEscaper, + "_html_template_urlescaper": urlEscaper, + "_html_template_urlfilter": urlFilter, + "_html_template_urlnormalizer": urlNormalizer, + "_eval_args_": evalArgs, } // escaper collects type inferences about templates and changes needed to make @@ -164,7 +163,6 @@ func (e *escaper) escape(c context, n parse.Node) context { panic("escaping " + n.String() + " is unimplemented") } -// Modified by Hugo. // var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") // escapeAction escapes an action template node. @@ -230,16 +228,8 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { c.jsCtx = jsCtxDivOp case stateJSDqStr, stateJSSqStr: s = append(s, "_html_template_jsstrescaper") - case stateJSBqStr: - if SecurityAllowActionJSTmpl.Load() { - // debugAllowActionJSTmpl.IncNonDefault() - s = append(s, "_html_template_jsstrescaper") - } else { - return context{ - state: stateError, - err: errorf(ErrJSTemplate, n, n.Line, "%s appears in a JS template literal", n), - } - } + case stateJSTmplLit: + s = append(s, "_html_template_jstmpllitescaper") case stateJSRegexp: s = append(s, "_html_template_jsregexpescaper") case stateCSS: @@ -398,6 +388,9 @@ var redundantFuncs = map[string]map[string]bool{ "_html_template_jsstrescaper": { "_html_template_attrescaper": true, }, + "_html_template_jstmpllitescaper": { + "_html_template_attrescaper": true, + }, "_html_template_urlescaper": { "_html_template_urlnormalizer": true, }, diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index 4ad5316fb..b202afde0 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -35,14 +35,14 @@ func (x *goodMarshaler) MarshalJSON() ([]byte, error) { func TestEscape(t *testing.T) { data := struct { - F, T bool - C, G, H string - A, E []string - B, M json.Marshaler - N int - U any // untyped nil - Z *int // typed nil - W htmltemplate.HTML + F, T bool + C, G, H, I string + A, E []string + B, M json.Marshaler + N int + U any // untyped nil + Z *int // typed nil + W htmltemplate.HTML }{ F: false, T: true, @@ -57,6 +57,7 @@ func TestEscape(t *testing.T) { U: nil, Z: nil, W: htmltemplate.HTML(`¡<b class="foo">Hello</b>, <textarea>O'World</textarea>!`), + I: "${ asd `` }", } pdata := &data @@ -723,6 +724,21 @@ func TestEscape(t *testing.T) { "<p name=\"{{.U}}\">", "<p name=\"\">", }, + { + "JS template lit special characters", + "<script>var a = `{{.I}}`</script>", + "<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>", + }, + { + "JS template lit special characters, nested lit", + "<script>var a = `${ `{{.I}}` }`</script>", + "<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>", + }, + { + "JS template lit, nested JS", + "<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>", + "<script>var a = `${ var a = \"a \\u0022 d\" }`</script>", + }, } for _, test := range tests { @@ -981,6 +997,31 @@ func TestErrors(t *testing.T) { "<script>var a = `${a+b}`</script>`", "", }, + { + "<script>var tmpl = `asd`;</script>", + ``, + }, + { + "<script>var tmpl = `${1}`;</script>", + ``, + }, + { + "<script>var tmpl = `${return ``}`;</script>", + ``, + }, + { + "<script>var tmpl = `${return {{.}} }`;</script>", + ``, + }, + { + "<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>", + ``, + }, + { + "<script>var tmpl = `asd ${return \"{\"}`;</script>", + ``, + }, + // Error cases. { "{{if .Cond}}<a{{end}}", @@ -1127,10 +1168,6 @@ func TestErrors(t *testing.T) { // html is allowed since it is the last command in the pipeline, but urlquery is not. `predefined escaper "urlquery" disallowed in template`, }, - { - "<script>var tmpl = `asd {{.}}`;</script>", - `{{.}} appears in a JS template literal`, - }, } for _, test := range tests { buf := new(bytes.Buffer) @@ -1354,7 +1391,7 @@ func TestEscapeText(t *testing.T) { }, { "<a onclick=\"`foo", - context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript}, + context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript}, }, { `<A ONCLICK="'`, @@ -1696,6 +1733,94 @@ func TestEscapeText(t *testing.T) { `<svg:a svg:onclick="x()">`, context{}, }, + { + "<script>var a = `", + context{state: stateJSTmplLit, element: elementScript}, + }, + { + "<script>var a = `${", + context{state: stateJS, element: elementScript}, + }, + { + "<script>var a = `${}", + context{state: stateJSTmplLit, element: elementScript}, + }, + { + "<script>var a = `${`", + context{state: stateJSTmplLit, element: elementScript}, + }, + { + "<script>var a = `${var a = \"", + context{state: stateJSDqStr, element: elementScript}, + }, + { + "<script>var a = `${var a = \"`", + context{state: stateJSDqStr, element: elementScript}, + }, + { + "<script>var a = `${var a = \"}", + context{state: stateJSDqStr, element: elementScript}, + }, + { + "<script>var a = `${``", + context{state: stateJS, element: elementScript}, + }, + { + "<script>var a = `${`}", + context{state: stateJSTmplLit, element: elementScript}, + }, + { + "<script>`${ {} } asd`</script><script>`${ {} }", + context{state: stateJSTmplLit, element: elementScript}, + }, + { + "<script>var foo = `${ (_ => { return \"x\" })() + \"${", + context{state: stateJSDqStr, element: elementScript}, + }, + { + "<script>var a = `${ {</script><script>var b = `${ x }", + context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp}, + }, + { + "<script>var foo = `x` + \"${", + context{state: stateJSDqStr, element: elementScript}, + }, + { + "<script>function f() { var a = `${}`; }", + context{state: stateJS, element: elementScript}, + }, + { + "<script>{`${}`}", + context{state: stateJS, element: elementScript}, + }, + { + "<script>`${ function f() { return `${1}` }() }`", + context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, + }, + { + "<script>function f() {`${ function f() { `${1}` } }`}", + context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp}, + }, + { + "<script>`${ { `` }", + context{state: stateJS, element: elementScript}, + }, + { + "<script>`${ { }`", + context{state: stateJSTmplLit, element: elementScript}, + }, + { + "<script>var foo = `${ foo({ a: { c: `${", + context{state: stateJS, element: elementScript}, + }, + { + "<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ", + context{state: stateJS, element: elementScript}, + }, + { + "<script>`${ `}", + context{state: stateJSTmplLit, element: elementScript}, + }, } for _, test := range tests { diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go index 8f9b893b4..e91aeafa1 100644 --- a/tpl/internal/go_templates/htmltemplate/exec_test.go +++ b/tpl/internal/go_templates/htmltemplate/exec_test.go @@ -325,12 +325,16 @@ var execTests = []execTest{ {"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, - {"nested assignment", + { + "nested assignment", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", - "3", tVal, true}, - {"nested assignment changes the last declaration", + "3", tVal, true, + }, + { + "nested assignment changes the last declaration", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", - "1", tVal, true}, + "1", tVal, true, + }, // Type with String method. {"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template @@ -377,15 +381,21 @@ var execTests = []execTest{ {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, - {"method on chained var", + { + "method on chained var", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method", + "true", tVal, true, + }, + { + "chained method", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - |