]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix upstream Go templates bug with reversed key/value assignment
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 15 Jun 2023 14:34:16 +0000 (16:34 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 15 Jun 2023 21:04:33 +0000 (23:04 +0200)
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:
config/allconfig/load.go
config/security/securityConfig.go
config/security/securityConfig_test.go
tpl/internal/go_templates/htmltemplate/context.go
tpl/internal/go_templates/htmltemplate/css.go
tpl/internal/go_templates/htmltemplate/css_test.go
tpl/internal/go_templates/htmltemplate/doc.go
tpl/internal/go_templates/htmltemplate/error.go
tpl/internal/go_templates/htmltemplate/escape.go
tpl/internal/go_templates/htmltemplate/escape_test.go
tpl/internal/go_templates/htmltemplate/html.go
tpl/internal/go_templates/htmltemplate/hugo_template.go
tpl/internal/go_templates/htmltemplate/js.go
tpl/internal/go_templates/htmltemplate/js_test.go
tpl/internal/go_templates/htmltemplate/jsctx_string.go
tpl/internal/go_templates/htmltemplate/state_string.go
tpl/internal/go_templates/htmltemplate/transition.go
tpl/internal/go_templates/testenv/exec.go
tpl/internal/go_templates/testenv/testenv.go
tpl/internal/go_templates/testenv/testenv_test.go
tpl/internal/go_templates/texttemplate/exec.go
tpl/internal/go_templates/texttemplate/exec_test.go
tpl/template.go
tpl/tplimpl/integration_test.go

index ad090d60d1a5647a9f6e8674fa2ed18aa9c5e78f..eca9d06dfa9a859be2726c5beadaa20962ea899a 100644 (file)
@@ -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
 
 }
index 8bd12af4b773463b3a5c4adf2ca90a807fae3342..5d0db2fb94053ffc38bc75db020a57733726b61a 100644 (file)
@@ -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()
index 3bfd59ce33b9ec7bb5851524c538550781f58a43..12ce3aae4497fb05fda944b0187a50470f09447e 100644 (file)
@@ -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 = ['.*']",
        )
 }
 
index 146a95d035caf4b64b8f5d306147b364714728a2..9f592b57f5a12cf9d43a161437e668cf3417150c 100644 (file)
@@ -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 */.
index 890a0c6b227feb0f58f9a80aa40b5356ef47a840..f650d8b3e843a358f62383f8524fd2e2d0b4de35 100644 (file)
@@ -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 -->.
index 7d8ad8b598990b7c70cdf48c0ab539e536cdf1cd..f44568930ddfc14857558afcc565050fe41db3f8 100644 (file)
@@ -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)
index 8422b49214b0c61f1c4198bebf7d6c85f7060f14..98b5658f40218b6bdc87e8460db23edd4e2240ce 100644 (file)
@@ -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
index 916b41a82c56111bbc4140638e44003540daf65d..0a62563cf4f4c1b117404038047ad2d8e0abbcea 100644 (file)
@@ -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 {
index 3aac865ef1b76b0b60b6cf4b93c36c620388d35b..ba9b4bbb8db945cce10beeaf351ba0819cb64756 100644 (file)
@@ -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,
index a08ea57ef70f07101a3b84463084be542fe346b9..680ba6fa714ddea2c90cdff9b061705d28520641 100644 (file)
@@ -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)
@@ -1308,6 +1327,10 @@ func TestEscapeText(t *testing.T) {
                        `<a onclick="'foo&quot;`,
                        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},
index bcca0b51a0ef901de193509f3fa709748b679381..a181699a5bda82fb0ad9df25a9eb6f576138c6bd 100644 (file)
@@ -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)
        }
index 99edf8f68167477411740e5ff13f9ad14f1a1356..98e03ad3c469c432960043143842b1fed1c5bd17 100644 (file)
 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
index 6187dc036344ee194c1476131a80d689140c80e5..3b5178eb12174f39b0edb8f54b79f0728ea6a580 100644 (file)
@@ -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`,
index 483a3694f5d3b43ad21a869d439853f06cb6c970..67a9213370b2ee1479dd44ce8df44ad84b7d9fb5 100644 (file)
@@ -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",
                },
index dd1d87ee454856a14f218c523d2aeda354aba8c6..23948934c950a10fb9e3533ac58f63eeb147bea0 100644 (file)
@@ -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}
index 05104be89c25459c6d704722f0e66c9bc5114a38..6fb1a6eeb0e746a4b2257bd6623ab96f28dc7f84 100644 (file)
@@ -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) {
index 06df679330db2c49d1bfc7d2369f2bc2a40f164a..92eb3519063c9ea0fd17eb8344939ab4871d8ba8 100644 (file)
@@ -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 = `\/[]`
        }
index ca4023647d4623b9dc3cfc8be8abc74c2397008d..13b4c7102e79a193b1dad6fae62a91244ea7cf2b 100644 (file)
@@ -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
index 6bfa54a979cd1f92949618bde4aa764e0376db07..91de6e76cbb660b2e4fa3ae5d28bac94d70f3914 100644 (file)
@@ -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")
 }
index 97c92a6e9498759fb5907ee991f958718d0498ec..2f72080c2b054ad28d8e39e4fd5c42440179d80f 100644 (file)
@@ -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
index 62b2e519ea07b23760c583aec22a3efb63292935..597866c6866ee18a621fc2dfcc6a04d345a8d6bf 100644 (file)
@@ -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)
                        }
                }
index 45edb9e9bb2941c7788bc295e12a6e3b2f9b133c..b44d61bb116422b665c464e4279c650ac1ee7424 100644 (file)
@@ -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 {
index 7a793101c512c9d708ba6c2e24c028bae0f5c687..91a482c25042c1c3edff49e38367b33f7d173fcd 100644 (file)
@@ -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
 }
index 4107a1faa35591dd2fb4dc150151bb29f6f86fb4..fa511fbab3542dd2ec276d06638dac15f1a9f4d3 100644 (file)
@@ -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()
+
+}