diff options
author | Bjørn Erik Pedersen <[email protected]> | 2023-06-15 16:34:16 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2023-06-15 23:04:33 +0200 |
commit | ee359df172ece11989e9b1bf35c2d376f2608ac6 (patch) | |
tree | 98513578d0ad8c2ced1c6aacf2ca5ba40a703b6a | |
parent | 0f989d5e21b200d848b45a4e305958636fd00779 (diff) | |
download | hugo-ee359df172ece11989e9b1bf35c2d376f2608ac6.tar.gz hugo-ee359df172ece11989e9b1bf35c2d376f2608ac6.zip |
Fix upstream Go templates bug with reversed key/value assignment
The template packages are based on go1.20.5 with the patch in befec5ddbbfbd81ec84e74e15a38044d67f8785b added.
This also includes a security fix that now disallows Go template actions in JS literals (inside backticks).
This will throw an error saying "... appears in a JS template literal".
If you're really sure this isn't a security risk in your case, you can revert to the old behaviour:
```toml
[security]
[security.gotemplates]
allowActionJSTmpl = true
```
See https://github.com/golang/go/issues/59234
Fixes #11112
24 files changed, 276 insertions, 143 deletions
diff --git a/config/allconfig/load.go b/config/allconfig/load.go index ad090d60d..eca9d06df 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -34,6 +34,7 @@ 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" ) @@ -89,6 +90,9 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { return nil, fmt.Errorf("failed to init config: %w", err) } + // This is unfortunate, but this is a global setting. + tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl) + return configs, nil } diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go index 8bd12af4b..5d0db2fb9 100644 --- a/config/security/securityConfig.go +++ b/config/security/securityConfig.go @@ -68,6 +68,9 @@ type Config struct { // Allow inline shortcodes EnableInlineShortcodes bool `json:"enableInlineShortcodes"` + + // Go templates related security config. + GoTemplates GoTemplates `json:"goTemplates"` } // Exec holds os/exec policies. @@ -93,6 +96,15 @@ 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() diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go index 3bfd59ce3..12ce3aae4 100644 --- a/config/security/securityConfig_test.go +++ b/config/security/securityConfig_test.go @@ -140,7 +140,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+)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\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+)$']\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 = ['.*']", ) } diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go index 146a95d03..9f592b57f 100644 --- a/tpl/internal/go_templates/htmltemplate/context.go +++ b/tpl/internal/go_templates/htmltemplate/context.go @@ -121,6 +121,8 @@ const ( stateJSDqStr // stateJSSqStr occurs inside a JavaScript single quoted string. stateJSSqStr + // stateJSBqStr occurs inside a JavaScript back quoted string. + stateJSBqStr // stateJSRegexp occurs inside a JavaScript regexp literal. stateJSRegexp // stateJSBlockCmt occurs inside a JavaScript /* block comment */. diff --git a/tpl/internal/go_templates/htmltemplate/css.go b/tpl/internal/go_templates/htmltemplate/css.go index 890a0c6b2..f650d8b3e 100644 --- a/tpl/internal/go_templates/htmltemplate/css.go +++ b/tpl/internal/go_templates/htmltemplate/css.go @@ -238,7 +238,7 @@ func cssValueFilter(args ...any) string { // inside a string that might embed JavaScript source. for i, c := range b { switch c { - case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}': + case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}', '<', '>': return filterFailsafe case '-': // Disallow <!-- or -->. diff --git a/tpl/internal/go_templates/htmltemplate/css_test.go b/tpl/internal/go_templates/htmltemplate/css_test.go index 7d8ad8b59..f44568930 100644 --- a/tpl/internal/go_templates/htmltemplate/css_test.go +++ b/tpl/internal/go_templates/htmltemplate/css_test.go @@ -234,6 +234,8 @@ func TestCSSValueFilter(t *testing.T) { {`-exp\000052 ession(alert(1337))`, "ZgotmplZ"}, {`-expre\0000073sion`, "-expre\x073sion"}, {`@import url evil.css`, "ZgotmplZ"}, + {"<", "ZgotmplZ"}, + {">", "ZgotmplZ"}, } for _, test := range tests { got := cssValueFilter(test.css) diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go index 8422b4921..98b5658f4 100644 --- a/tpl/internal/go_templates/htmltemplate/doc.go +++ b/tpl/internal/go_templates/htmltemplate/doc.go @@ -231,5 +231,12 @@ Least Surprise Property: "A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who knows that contextual autoescaping happens should be able to look at a {{.}} and correctly infer what sanitization happens." + +As a consequence of the Least Surprise Property, template actions within an +ECMAScript 6 template literal are disabled by default. +Handling string interpolation within these literals is rather complex resulting +in no clear safe way to support it. +To re-enable template actions within ECMAScript 6 template literals, use the +GODEBUG=jstmpllitinterp=1 environment variable. */ package template diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go index 916b41a82..0a62563cf 100644 --- a/tpl/internal/go_templates/htmltemplate/error.go +++ b/tpl/internal/go_templates/htmltemplate/error.go @@ -215,6 +215,19 @@ const ( // pipeline occurs in an unquoted attribute value context, "html" is // disallowed. Avoid using "html" and "urlquery" entirely in new templates. ErrPredefinedEscaper + + // errJSTmplLit: "... appears in a JS template literal" + // Example: + // <script>var tmpl = `{{.Interp}`</script> + // Discussion: + // Package html/template does not support actions inside of JS template + // literals. + // + // TODO(rolandshoemaker): we cannot add this as an exported error in a minor + // release, since it is backwards incompatible with the other minor + // releases. As such we need to leave it unexported, and then we'll add it + // in the next major release. + errJSTmplLit ) func (e *Error) Error() string { diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index 3aac865ef..ba9b4bbb8 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -161,6 +161,8 @@ func (e *escaper) escape(c context, n parse.Node) context { panic("escaping " + n.String() + " is unimplemented") } +// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") + // escapeAction escapes an action template node. func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { if len(n.Pipe.Decl) != 0 { @@ -224,6 +226,15 @@ 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() { // .Value() == "1" { + s = append(s, "_html_template_jsstrescaper") + } else { + return context{ + state: stateError, + err: errorf(errJSTmplLit, n, n.Line, "%s appears in a JS template literal", n), + } + } case stateJSRegexp: s = append(s, "_html_template_jsregexpescaper") case stateCSS: @@ -370,9 +381,8 @@ func normalizeEscFn(e string) string { // for all x. var redundantFuncs = map[string]map[string]bool{ "_html_template_commentescaper": { - "_html_template_attrescaper": true, - "_html_template_nospaceescaper": true, - "_html_template_htmlescaper": true, + "_html_template_attrescaper": true, + "_html_template_htmlescaper": true, }, "_html_template_cssescaper": { "_html_template_attrescaper": true, diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index a08ea57ef..680ba6fa7 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -683,38 +683,49 @@ func TestEscape(t *testing.T) { `<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`, `<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`, }, + { + "unquoted empty attribute value (plaintext)", + "<p name={{.U}}>", + "<p name=ZgotmplZ>", + }, + { + "unquoted empty attribute value (url)", + "<p href={{.U}}>", + "<p href=ZgotmplZ>", + }, + { + "quoted empty attribute value", + "<p name=\"{{.U}}\">", + "<p name=\"\">", + }, } for _, test := range tests { - tmpl := New(test.name) - tmpl = Must(tmpl.Parse(test.input)) - // Check for bug 6459: Tree field was not set in Parse. - if tmpl.Tree != tmpl.text.Tree { - t.Errorf("%s: tree not set properly", test.name) - continue - } - b := new(strings.Builder) - if err := tmpl.Execute(b, data); err != nil { - t.Errorf("%s: template execution failed: %s", test.name, err) - continue - } - if w, g := test.output, b.String(); w != g { - t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) - continue - } - b.Reset() - if err := tmpl.Execute(b, pdata); err != nil { - t.Errorf("%s: template execution failed for pointer: %s", test.name, err) - continue - } - if w, g := test.output, b.String(); w != g { - t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) - continue - } - if tmpl.Tree != tmpl.text.Tree { - t.Errorf("%s: tree mismatch", test.name) - continue - } + t.Run(test.name, func(t *testing.T) { + tmpl := New(test.name) + tmpl = Must(tmpl.Parse(test.input)) + // Check for bug 6459: Tree field was not set in Parse. + if tmpl.Tree != tmpl.text.Tree { + t.Fatalf("%s: tree not set properly", test.name) + } + b := new(strings.Builder) + if err := tmpl.Execute(b, data); err != nil { + t.Fatalf("%s: template execution failed: %s", test.name, err) + } + if w, g := test.output, b.String(); w != g { + t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) + } + b.Reset() + if err := tmpl.Execute(b, pdata); err != nil { + t.Fatalf("%s: template execution failed for pointer: %s", test.name, err) + } + if w, g := test.output, b.String(); w != g { + t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) + } + if tmpl.Tree != tmpl.text.Tree { + t.Fatalf("%s: tree mismatch", test.name) + } + }) } } @@ -941,6 +952,10 @@ func TestErrors(t *testing.T) { "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}", "", }, + { + "<script>var a = `${a+b}`</script>`", + "", + }, // Error cases. { "{{if .Cond}}<a{{end}}", @@ -1087,6 +1102,10 @@ 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) @@ -1309,6 +1328,10 @@ func TestEscapeText(t *testing.T) { context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, }, { + "<a onclick=\"`foo", + context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript}, + }, + { `<A ONCLICK="'`, context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, }, diff --git a/tpl/internal/go_templates/htmltemplate/html.go b/tpl/internal/go_templates/htmltemplate/html.go index bcca0b51a..a181699a5 100644 --- a/tpl/internal/go_templates/htmltemplate/html.go +++ b/tpl/internal/go_templates/htmltemplate/html.go @@ -14,6 +14,9 @@ import ( // htmlNospaceEscaper escapes for inclusion in unquoted attribute values. func htmlNospaceEscaper(args ...any) string { s, t := stringify(args...) + if s == "" { + return filterFailsafe + } if t == contentTypeHTML { return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false) } diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go index 99edf8f68..98e03ad3c 100644 --- a/tpl/internal/go_templates/htmltemplate/hugo_template.go +++ b/tpl/internal/go_templates/htmltemplate/hugo_template.go @@ -14,9 +14,15 @@ package template import ( + "sync/atomic" + template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" ) +// See https://github.com/golang/go/issues/59234 +// Moved here to avoid dependency on Go's internal/debug package. +var SecurityAllowActionJSTmpl atomic.Bool + /* This files contains the Hugo related addons. All the other files in this diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go index 6187dc036..3b5178eb1 100644 --- a/tpl/internal/go_templates/htmltemplate/js.go +++ b/tpl/internal/go_templates/htmltemplate/js.go @@ -14,6 +14,11 @@ import ( "unicode/utf8" ) +// jsWhitespace contains all of the JS whitespace characters, as defined +// by the \s character class. +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes. +const jsWhitespace = "\f\n\r\t\v\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff" + // nextJSCtx returns the context that determines whether a slash after the // given run of tokens starts a regular expression instead of a division // operator: / or /=. @@ -27,7 +32,8 @@ import ( // JavaScript 2.0 lexical grammar and requires one token of lookbehind: // https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html func nextJSCtx(s []byte, preceding jsCtx) jsCtx { - s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029") + // Trim all JS whitespace characters + s = bytes.TrimRight(s, jsWhitespace) if len(s) == 0 { return preceding } @@ -309,6 +315,7 @@ var jsStrReplacementTable = []string{ // Encode HTML specials as hex so the output can be embedded // in HTML attributes without further encoding. '"': `\u0022`, + '`': `\u0060`, '&': `\u0026`, '\'': `\u0027`, '+': `\u002b`, @@ -332,6 +339,7 @@ var jsStrNormReplacementTable = []string{ '"': `\u0022`, '&': `\u0026`, '\'': `\u0027`, + '`': `\u0060`, '+': `\u002b`, '/': `\/`, '<': `\u003c`, diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go index 483a3694f..67a921337 100644 --- a/tpl/internal/go_templates/htmltemplate/js_test.go +++ b/tpl/internal/go_templates/htmltemplate/js_test.go @@ -83,14 +83,17 @@ func TestNextJsCtx(t *testing.T) { {jsCtxDivOp, "0"}, // Dots that are part of a number are div preceders. {jsCtxDivOp, "0."}, + // Some JS interpreters treat NBSP as a normal space, so + // we must too in order to properly escape things. + {jsCtxRegexp, "=\u00A0"}, } for _, test := range tests { - if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx { - t.Errorf("want %s got %q", test.jsCtx, test.s) + if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx { + t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx) } - if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx { - t.Errorf("want %s got %q", test.jsCtx, test.s) + if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx { + t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx) } } @@ -294,7 +297,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { `0123456789:;\u003c=\u003e?` + `@ABCDEFGHIJKLMNO` + `PQRSTUVWXYZ[\\]^_` + - "`abcdefghijklmno" + + "\\u0060abcdefghijklmno" + "pqrstuvwxyz{|}~\u007f" + "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", }, diff --git a/tpl/internal/go_templates/htmltemplate/jsctx_string.go b/tpl/internal/go_templates/htmltemplate/jsctx_string.go index dd1d87ee4..23948934c 100644 --- a/tpl/internal/go_templates/htmltemplate/jsctx_string.go +++ b/tpl/internal/go_templates/htmltemplate/jsctx_string.go @@ -4,6 +4,15 @@ package template import "strconv" +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[jsCtxRegexp-0] + _ = x[jsCtxDivOp-1] + _ = x[jsCtxUnknown-2] +} + const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown" var _jsCtx_index = [...]uint8{0, 11, 21, 33} diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go index 05104be89..6fb1a6eeb 100644 --- a/tpl/internal/go_templates/htmltemplate/state_string.go +++ b/tpl/internal/go_templates/htmltemplate/state_string.go @@ -4,9 +4,42 @@ package template import "strconv" -const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateError" +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[stateText-0] + _ = x[stateTag-1] + _ = x[stateAttrName-2] + _ = x[stateAfterName-3] + _ = x[stateBeforeValue-4] + _ = x[stateHTMLCmt-5] + _ = x[stateRCDATA-6] + _ = x[stateAttr-7] + _ = x[stateURL-8] + _ = x[stateSrcset-9] + _ = x[stateJS-10] + _ = x[stateJSDqStr-11] + _ = x[stateJSSqStr-12] + _ = x[stateJSBqStr-13] + _ = x[stateJSRegexp-14] + _ = x[stateJSBlockCmt-15] + _ = x[stateJSLineCmt-16] + _ = x[stateCSS-17] + _ = x[stateCSSDqStr-18] + _ = x[stateCSSSqStr-19] + _ = x[stateCSSDqURL-20] + _ = x[stateCSSSqURL-21] + _ = x[stateCSSURL-22] + _ = x[stateCSSBlockCmt-23] + _ = x[stateCSSLineCmt-24] + _ = x[stateError-25] + _ = x[stateDead-26] +} + +const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead" -var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 155, 170, 184, 192, 205, 218, 231, 244, 255, 271, 286, 296} +var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 204, 217, 230, 243, 256, 267, 283, 298, 308, 317} func (i state) String() string { if i >= state(len(_state_index)-1) { diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go index 06df67933..92eb35190 100644 --- a/tpl/internal/go_templates/htmltemplate/transition.go +++ b/tpl/internal/go_templates/htmltemplate/transition.go @@ -27,6 +27,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){ stateJS: tJS, stateJSDqStr: tJSDelimited, stateJSSqStr: tJSDelimited, + stateJSBqStr: tJSDelimited, stateJSRegexp: tJSDelimited, stateJSBlockCmt: tBlockCmt, stateJSLineCmt: tLineCmt, @@ -262,7 +263,7 @@ func tURL(c context, s []byte) (context, int) { // tJS is the context transition function for the JS state. func tJS(c context, s []byte) (context, int) { - i := bytes.IndexAny(s, `"'/`) + i := bytes.IndexAny(s, "\"`'/") if i == -1 { // Entire input is non string, comment, regexp tokens. c.jsCtx = nextJSCtx(s, c.jsCtx) @@ -274,6 +275,8 @@ func tJS(c context, s []byte) (context, int) { c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp case '\'': c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp + case '`': + c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp case '/': switch { case i+1 < len(s) && s[i+1] == '/': @@ -303,6 +306,8 @@ func tJSDelimited(c context, s []byte) (context, int) { switch c.state { case stateJSSqStr: specials = `\'` + case stateJSBqStr: + specials = "`\\" case stateJSRegexp: specials = `\/[]` } diff --git a/tpl/internal/go_templates/testenv/exec.go b/tpl/internal/go_templates/testenv/exec.go index ca4023647..13b4c7102 100644 --- a/tpl/internal/go_templates/testenv/exec.go +++ b/tpl/internal/go_templates/testenv/exec.go @@ -9,11 +9,9 @@ import ( "os" "os/exec" "runtime" - "strconv" "strings" "sync" "testing" - "time" ) // HasExec reports whether the current system can start new processes @@ -84,87 +82,7 @@ func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { // - fails the test if the command does not complete before the test's deadline, and // - sets a Cleanup function that verifies that the test did not leak a subprocess. func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { - t.Helper() - MustHaveExec(t) - - var ( - cancelCtx context.CancelFunc - gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) - ) - - if t, ok := t.(interface { - testing.TB - Deadline() (time.Time, bool) - }); ok { - if td, ok := t.Deadline(); ok { - // Start with a minimum grace period, just long enough to consume the - // output of a reasonable program after it terminates. - gracePeriod = 100 * time.Millisecond - if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { - scale, err := strconv.Atoi(s) - if err != nil { - t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) - } - gracePeriod *= time.Duration(scale) - } - - // If time allows, increase the termination grace period to 5% of the - // test's remaining time. - testTimeout := time.Until(td) - if gp := testTimeout / 20; gp > gracePeriod { - gracePeriod = gp - } - - // When we run commands that execute subprocesses, we want to reserve two - // grace periods to clean up: one for the delay between the first - // termination signal being sent (via the Cancel callback when the Context - // expires) and the process being forcibly terminated (via the WaitDelay - // field), and a second one for the delay becween the process being - // terminated and and the test logging its output for debugging. - // - // (We want to ensure that the test process itself has enough time to - // log the output before it is also terminated.) - cmdTimeout := testTimeout - 2*gracePeriod - - if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { - // Either ctx doesn't have a deadline, or its deadline would expire - // after (or too close before) the test has already timed out. - // Add a shorter timeout so that the test will produce useful output. - ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) - } - } - } - - cmd := exec.CommandContext(ctx, name, args...) - /*cmd.Cancel = func() error { - if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { - // The command timed out due to running too close to the test's deadline. - // There is no way the test did that intentionally — it's too close to the - // wire! — so mark it as a test failure. That way, if the test expects the - // command to fail for some other reason, it doesn't have to distinguish - // between that reason and a timeout. - t.Errorf("test timed out while running command: %v", cmd) - } else { - // The command is being terminated due to ctx being canceled, but - // apparently not due to an explicit test deadline that we added. - // Log that information in case it is useful for diagnosing a failure, - // but don't actually fail the test because of it. - t.Logf("%v: terminating command: %v", ctx.Err(), cmd) - } - return cmd.Process.Signal(Sigquit) - } - cmd.WaitDelay = gracePeriod*/ - - t.Cleanup(func() { - if cancelCtx != nil { - cancelCtx() - } - if cmd.Process != nil && cmd.ProcessState == nil { - t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) - } - }) - - return cmd + panic("Not implemented, Hugo is not using this") } // Command is like exec.Command, but applies the same changes as diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index 6bfa54a97..91de6e76c 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -258,7 +258,7 @@ func MustHaveCGO(t testing.TB) { // CanInternalLink reports whether the current system can link programs with // internal linking. func CanInternalLink() bool { - return false + panic("not implemented, not needed by Hugo") } // MustInternalLink checks that the current system can link programs with internal @@ -349,15 +349,5 @@ func SkipIfOptimizationOff(t testing.TB) { // dstPath containing entries for the packages in std and cmd in addition // to the package to package file mappings in additionalPackageFiles. func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) { - /*importcfg, err := goroot.Importcfg() - for k, v := range additionalPackageFiles { - importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v) - } - if err != nil { - t.Fatalf("preparing the importcfg failed: %s", err) - } - err = os.WriteFile(dstPath, []byte(importcfg), 0655) - if err != nil { - t.Fatalf("writing the importcfg failed: %s", err) - }*/ + panic("not implemented, not needed by Hugo") } diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go index 97c92a6e9..2f72080c2 100644 --- a/tpl/internal/go_templates/testenv/testenv_test.go +++ b/tpl/internal/go_templates/testenv/testenv_test.go @@ -13,7 +13,8 @@ import ( "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv" ) -func _TestGoToolLocation(t *testing.T) { +func TestGoToolLocation(t *testing.T) { + t.Skip("skipping test that requires go command") testenv.MustHaveGoBuild(t) var exeSuffix string diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index 62b2e519e..597866c68 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -361,19 +361,27 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { // mark top of stack before any variables in the body are pushed. mark := s.mark() oneIteration := func(index, elem reflect.Value) { - // Set top var (lexically the second if there are two) to the element. if len(r.Pipe.Decl) > 0 { if r.Pipe.IsAssign { - s.setVar(r.Pipe.Decl[0].Ident[0], elem) + // With two variables, index comes first. + // With one, we use the element. + if len(r.Pipe.Decl) > 1 { + s.setVar(r.Pipe.Decl[0].Ident[0], index) + } else { + s.setVar(r.Pipe.Decl[0].Ident[0], elem) + } } else { + // Set top var (lexically the second if there + // are two) to the element. s.setTopVar(1, elem) } } - // Set next var (lexically the first if there are two) to the index. if len(r.Pipe.Decl) > 1 { if r.Pipe.IsAssign { - s.setVar(r.Pipe.Decl[1].Ident[0], index) + s.setVar(r.Pipe.Decl[1].Ident[0], elem) } else { + // Set next var (lexically the first if there + // are two) to the index. s.setTopVar(2, index) } } diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go index 45edb9e9b..b44d61bb1 100644 --- a/tpl/internal/go_templates/texttemplate/exec_test.go +++ b/tpl/internal/go_templates/texttemplate/exec_test.go @@ -697,6 +697,7 @@ var execTests = []execTest{ {"bug18c", "{{eq . 'P'}}", "true", 'P', true}, {"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true}, + {"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true}, } func zeroArgs() string { diff --git a/tpl/template.go b/tpl/template.go index 7a793101c..91a482c25 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -169,6 +169,13 @@ func SetPageInContext(ctx context.Context, p page) context.Context { return context.WithValue(ctx, texttemplate.PageContextKey, p) } +// SetSecurityAllowActionJSTmpl sets the global setting for allowing tempalte actions in JS template literals. +// This was added in Hugo 0.114.0. +// See https://github.com/golang/go/issues/59234 +func SetSecurityAllowActionJSTmpl(b bool) { + htmltemplate.SecurityAllowActionJSTmpl.Store(b) +} + type page interface { IsNode() bool } diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go index 4107a1faa..fa511fbab 100644 --- a/tpl/tplimpl/integration_test.go +++ b/tpl/tplimpl/integration_test.go @@ -2,6 +2,7 @@ package tplimpl_test import ( "path/filepath" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -160,3 +161,70 @@ title: "S3P1" b.AssertFileContent("public/s2/p1/index.html", `S2P1`) b.AssertFileContent("public/s3/p1/index.html", `S3P1`) } + +func TestGoTemplateBugs(t *testing.T) { + + t.Run("Issue 11112", func(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +-- layouts/index.html -- +{{ $m := dict "key" "value" }} +{{ $k := "" }} +{{ $v := "" }} +{{ range $k, $v = $m }} +{{ $k }} = {{ $v }} +{{ end }} + ` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ) + b.Build() + + b.AssertFileContent("public/index.html", `key = value`) + }) + +} + +func TestSecurityAllowActionJSTmpl(t *testing.T) { + + filesTemplate := ` +-- config.toml -- +SECURITYCONFIG +-- layouts/index.html -- +<script> +var a = §§{{.Title }}§§; +</script> + ` + + files := strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", "") + + b, err := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).BuildE() + + b.Assert(err, qt.Not(qt.IsNil)) + b.Assert(err.Error(), qt.Contains, "{{.Title}} appears in a JS template literal") + + files = strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", ` +[security] +[security.gotemplates] +allowActionJSTmpl = true +`) + + b = hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + +} |