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
This commit is contained in:
Bjørn Erik Pedersen 2023-06-15 16:34:16 +02:00
parent 0f989d5e21
commit ee359df172
24 changed files with 276 additions and 143 deletions

View file

@ -34,6 +34,7 @@ import (
hglob "github.com/gohugoio/hugo/hugofs/glob" hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/tpl"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -89,6 +90,9 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
return nil, fmt.Errorf("failed to init config: %w", err) 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 return configs, nil
} }

View file

@ -68,6 +68,9 @@ type Config struct {
// Allow inline shortcodes // Allow inline shortcodes
EnableInlineShortcodes bool `json:"enableInlineShortcodes"` EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
// Go templates related security config.
GoTemplates GoTemplates `json:"goTemplates"`
} }
// Exec holds os/exec policies. // Exec holds os/exec policies.
@ -93,6 +96,15 @@ type HTTP struct {
MediaTypes Whitelist `json:"mediaTypes"` 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. // ToTOML converts c to TOML with [security] as the root.
func (c Config) ToTOML() string { func (c Config) ToTOML() string {
sec := c.ToSecurityMap() sec := c.ToSecurityMap()

View file

@ -140,7 +140,7 @@ func TestToTOML(t *testing.T) {
got := DefaultConfig.ToTOML() got := DefaultConfig.ToTOML()
c.Assert(got, qt.Equals, 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 = ['.*']",
) )
} }

View file

@ -121,6 +121,8 @@ const (
stateJSDqStr stateJSDqStr
// stateJSSqStr occurs inside a JavaScript single quoted string. // stateJSSqStr occurs inside a JavaScript single quoted string.
stateJSSqStr stateJSSqStr
// stateJSBqStr occurs inside a JavaScript back quoted string.
stateJSBqStr
// stateJSRegexp occurs inside a JavaScript regexp literal. // stateJSRegexp occurs inside a JavaScript regexp literal.
stateJSRegexp stateJSRegexp
// stateJSBlockCmt occurs inside a JavaScript /* block comment */. // stateJSBlockCmt occurs inside a JavaScript /* block comment */.

View file

@ -238,7 +238,7 @@ func cssValueFilter(args ...any) string {
// inside a string that might embed JavaScript source. // inside a string that might embed JavaScript source.
for i, c := range b { for i, c := range b {
switch c { switch c {
case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}': case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}', '<', '>':
return filterFailsafe return filterFailsafe
case '-': case '-':
// Disallow <!-- or -->. // Disallow <!-- or -->.

View file

@ -234,6 +234,8 @@ func TestCSSValueFilter(t *testing.T) {
{`-exp\000052 ession(alert(1337))`, "ZgotmplZ"}, {`-exp\000052 ession(alert(1337))`, "ZgotmplZ"},
{`-expre\0000073sion`, "-expre\x073sion"}, {`-expre\0000073sion`, "-expre\x073sion"},
{`@import url evil.css`, "ZgotmplZ"}, {`@import url evil.css`, "ZgotmplZ"},
{"<", "ZgotmplZ"},
{">", "ZgotmplZ"},
} }
for _, test := range tests { for _, test := range tests {
got := cssValueFilter(test.css) got := cssValueFilter(test.css)

View file

@ -231,5 +231,12 @@ Least Surprise Property:
"A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who "A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who
knows that contextual autoescaping happens should be able to look at a {{.}} knows that contextual autoescaping happens should be able to look at a {{.}}
and correctly infer what sanitization happens." 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 package template

View file

@ -215,6 +215,19 @@ const (
// pipeline occurs in an unquoted attribute value context, "html" is // pipeline occurs in an unquoted attribute value context, "html" is
// disallowed. Avoid using "html" and "urlquery" entirely in new templates. // disallowed. Avoid using "html" and "urlquery" entirely in new templates.
ErrPredefinedEscaper 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 { func (e *Error) Error() string {

View file

@ -161,6 +161,8 @@ func (e *escaper) escape(c context, n parse.Node) context {
panic("escaping " + n.String() + " is unimplemented") panic("escaping " + n.String() + " is unimplemented")
} }
// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
// escapeAction escapes an action template node. // escapeAction escapes an action template node.
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
if len(n.Pipe.Decl) != 0 { if len(n.Pipe.Decl) != 0 {
@ -224,6 +226,15 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
c.jsCtx = jsCtxDivOp c.jsCtx = jsCtxDivOp
case stateJSDqStr, stateJSSqStr: case stateJSDqStr, stateJSSqStr:
s = append(s, "_html_template_jsstrescaper") 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: case stateJSRegexp:
s = append(s, "_html_template_jsregexpescaper") s = append(s, "_html_template_jsregexpescaper")
case stateCSS: case stateCSS:
@ -370,9 +381,8 @@ func normalizeEscFn(e string) string {
// for all x. // for all x.
var redundantFuncs = map[string]map[string]bool{ var redundantFuncs = map[string]map[string]bool{
"_html_template_commentescaper": { "_html_template_commentescaper": {
"_html_template_attrescaper": true, "_html_template_attrescaper": true,
"_html_template_nospaceescaper": true, "_html_template_htmlescaper": true,
"_html_template_htmlescaper": true,
}, },
"_html_template_cssescaper": { "_html_template_cssescaper": {
"_html_template_attrescaper": true, "_html_template_attrescaper": true,

View file

@ -683,38 +683,49 @@ func TestEscape(t *testing.T) {
`<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`, `<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`,
`<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 { for _, test := range tests {
tmpl := New(test.name) t.Run(test.name, func(t *testing.T) {
tmpl = Must(tmpl.Parse(test.input)) tmpl := New(test.name)
// Check for bug 6459: Tree field was not set in Parse. tmpl = Must(tmpl.Parse(test.input))
if tmpl.Tree != tmpl.text.Tree { // Check for bug 6459: Tree field was not set in Parse.
t.Errorf("%s: tree not set properly", test.name) if tmpl.Tree != tmpl.text.Tree {
continue t.Fatalf("%s: tree not set properly", test.name)
} }
b := new(strings.Builder) b := new(strings.Builder)
if err := tmpl.Execute(b, data); err != nil { if err := tmpl.Execute(b, data); err != nil {
t.Errorf("%s: template execution failed: %s", test.name, err) t.Fatalf("%s: template execution failed: %s", test.name, err)
continue }
} if w, g := test.output, b.String(); w != g {
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)
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 {
b.Reset() t.Fatalf("%s: template execution failed for pointer: %s", test.name, err)
if err := tmpl.Execute(b, pdata); err != nil { }
t.Errorf("%s: template execution failed for pointer: %s", test.name, err) if w, g := test.output, b.String(); w != g {
continue t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
} }
if w, g := test.output, b.String(); w != g { if tmpl.Tree != tmpl.text.Tree {
t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) t.Fatalf("%s: tree mismatch", test.name)
continue }
} })
if tmpl.Tree != tmpl.text.Tree {
t.Errorf("%s: tree mismatch", test.name)
continue
}
} }
} }
@ -941,6 +952,10 @@ func TestErrors(t *testing.T) {
"{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}", "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
"", "",
}, },
{
"<script>var a = `${a+b}`</script>`",
"",
},
// Error cases. // Error cases.
{ {
"{{if .Cond}}<a{{end}}", "{{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. // html is allowed since it is the last command in the pipeline, but urlquery is not.
`predefined escaper "urlquery" disallowed in template`, `predefined escaper "urlquery" disallowed in template`,
}, },
{
"<script>var tmpl = `asd {{.}}`;</script>",
`{{.}} appears in a JS template literal`,
},
} }
for _, test := range tests { for _, test := range tests {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
@ -1308,6 +1327,10 @@ func TestEscapeText(t *testing.T) {
`<a onclick="'foo&quot;`, `<a onclick="'foo&quot;`,
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
}, },
{
"<a onclick=\"`foo",
context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript},
},
{ {
`<A ONCLICK="'`, `<A ONCLICK="'`,
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript}, context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},

View file

@ -14,6 +14,9 @@ import (
// htmlNospaceEscaper escapes for inclusion in unquoted attribute values. // htmlNospaceEscaper escapes for inclusion in unquoted attribute values.
func htmlNospaceEscaper(args ...any) string { func htmlNospaceEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if s == "" {
return filterFailsafe
}
if t == contentTypeHTML { if t == contentTypeHTML {
return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false) return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false)
} }

View file

@ -14,9 +14,15 @@
package template package template
import ( import (
"sync/atomic"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" 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 This files contains the Hugo related addons. All the other files in this

View file

@ -14,6 +14,11 @@ import (
"unicode/utf8" "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 // nextJSCtx returns the context that determines whether a slash after the
// given run of tokens starts a regular expression instead of a division // given run of tokens starts a regular expression instead of a division
// operator: / or /=. // operator: / or /=.
@ -27,7 +32,8 @@ import (
// JavaScript 2.0 lexical grammar and requires one token of lookbehind: // JavaScript 2.0 lexical grammar and requires one token of lookbehind:
// https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html // https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html
func nextJSCtx(s []byte, preceding jsCtx) jsCtx { 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 { if len(s) == 0 {
return preceding return preceding
} }
@ -309,6 +315,7 @@ var jsStrReplacementTable = []string{
// Encode HTML specials as hex so the output can be embedded // Encode HTML specials as hex so the output can be embedded
// in HTML attributes without further encoding. // in HTML attributes without further encoding.
'"': `\u0022`, '"': `\u0022`,
'`': `\u0060`,
'&': `\u0026`, '&': `\u0026`,
'\'': `\u0027`, '\'': `\u0027`,
'+': `\u002b`, '+': `\u002b`,
@ -332,6 +339,7 @@ var jsStrNormReplacementTable = []string{
'"': `\u0022`, '"': `\u0022`,
'&': `\u0026`, '&': `\u0026`,
'\'': `\u0027`, '\'': `\u0027`,
'`': `\u0060`,
'+': `\u002b`, '+': `\u002b`,
'/': `\/`, '/': `\/`,
'<': `\u003c`, '<': `\u003c`,

View file

@ -83,14 +83,17 @@ func TestNextJsCtx(t *testing.T) {
{jsCtxDivOp, "0"}, {jsCtxDivOp, "0"},
// Dots that are part of a number are div preceders. // Dots that are part of a number are div preceders.
{jsCtxDivOp, "0."}, {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 { for _, test := range tests {
if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx { if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx {
t.Errorf("want %s got %q", test.jsCtx, test.s) t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
} }
if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx { if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx {
t.Errorf("want %s got %q", test.jsCtx, test.s) t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
} }
} }
@ -294,7 +297,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
`0123456789:;\u003c=\u003e?` + `0123456789:;\u003c=\u003e?` +
`@ABCDEFGHIJKLMNO` + `@ABCDEFGHIJKLMNO` +
`PQRSTUVWXYZ[\\]^_` + `PQRSTUVWXYZ[\\]^_` +
"`abcdefghijklmno" + "\\u0060abcdefghijklmno" +
"pqrstuvwxyz{|}~\u007f" + "pqrstuvwxyz{|}~\u007f" +
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
}, },

View file

@ -4,6 +4,15 @@ package template
import "strconv" 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" const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
var _jsCtx_index = [...]uint8{0, 11, 21, 33} var _jsCtx_index = [...]uint8{0, 11, 21, 33}

View file

@ -4,9 +4,42 @@ package template
import "strconv" 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]
}
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} const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
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 { func (i state) String() string {
if i >= state(len(_state_index)-1) { if i >= state(len(_state_index)-1) {

View file

@ -27,6 +27,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){
stateJS: tJS, stateJS: tJS,
stateJSDqStr: tJSDelimited, stateJSDqStr: tJSDelimited,
stateJSSqStr: tJSDelimited, stateJSSqStr: tJSDelimited,
stateJSBqStr: tJSDelimited,
stateJSRegexp: tJSDelimited, stateJSRegexp: tJSDelimited,
stateJSBlockCmt: tBlockCmt, stateJSBlockCmt: tBlockCmt,
stateJSLineCmt: tLineCmt, stateJSLineCmt: tLineCmt,
@ -262,7 +263,7 @@ func tURL(c context, s []byte) (context, int) {
// tJS is the context transition function for the JS state. // tJS is the context transition function for the JS state.
func tJS(c context, s []byte) (context, int) { func tJS(c context, s []byte) (context, int) {
i := bytes.IndexAny(s, `"'/`) i := bytes.IndexAny(s, "\"`'/")
if i == -1 { if i == -1 {
// Entire input is non string, comment, regexp tokens. // Entire input is non string, comment, regexp tokens.
c.jsCtx = nextJSCtx(s, c.jsCtx) c.jsCtx = nextJSCtx(s, c.jsCtx)
@ -274,6 +275,8 @@ func tJS(c context, s []byte) (context, int) {
c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
case '\'': case '\'':
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
case '`':
c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp
case '/': case '/':
switch { switch {
case i+1 < len(s) && s[i+1] == '/': case i+1 < len(s) && s[i+1] == '/':
@ -303,6 +306,8 @@ func tJSDelimited(c context, s []byte) (context, int) {
switch c.state { switch c.state {
case stateJSSqStr: case stateJSSqStr:
specials = `\'` specials = `\'`
case stateJSBqStr:
specials = "`\\"
case stateJSRegexp: case stateJSRegexp:
specials = `\/[]` specials = `\/[]`
} }

View file

@ -9,11 +9,9 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
) )
// HasExec reports whether the current system can start new processes // 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 // - 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. // - 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 { func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd {
t.Helper() panic("Not implemented, Hugo is not using this")
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
} }
// Command is like exec.Command, but applies the same changes as // Command is like exec.Command, but applies the same changes as

View file

@ -258,7 +258,7 @@ func MustHaveCGO(t testing.TB) {
// CanInternalLink reports whether the current system can link programs with // CanInternalLink reports whether the current system can link programs with
// internal linking. // internal linking.
func CanInternalLink() bool { func CanInternalLink() bool {
return false panic("not implemented, not needed by Hugo")
} }
// MustInternalLink checks that the current system can link programs with internal // 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 // dstPath containing entries for the packages in std and cmd in addition
// to the package to package file mappings in additionalPackageFiles. // to the package to package file mappings in additionalPackageFiles.
func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) { func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) {
/*importcfg, err := goroot.Importcfg() panic("not implemented, not needed by Hugo")
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)
}*/
} }

View file

@ -13,7 +13,8 @@ import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv" "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) testenv.MustHaveGoBuild(t)
var exeSuffix string var exeSuffix string

View 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 top of stack before any variables in the body are pushed.
mark := s.mark() mark := s.mark()
oneIteration := func(index, elem reflect.Value) { 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 len(r.Pipe.Decl) > 0 {
if r.Pipe.IsAssign { 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 { } else {
// Set top var (lexically the second if there
// are two) to the element.
s.setTopVar(1, elem) s.setTopVar(1, elem)
} }
} }
// Set next var (lexically the first if there are two) to the index.
if len(r.Pipe.Decl) > 1 { if len(r.Pipe.Decl) > 1 {
if r.Pipe.IsAssign { if r.Pipe.IsAssign {
s.setVar(r.Pipe.Decl[1].Ident[0], index) s.setVar(r.Pipe.Decl[1].Ident[0], elem)
} else { } else {
// Set next var (lexically the first if there
// are two) to the index.
s.setTopVar(2, index) s.setTopVar(2, index)
} }
} }

View file

@ -697,6 +697,7 @@ var execTests = []execTest{
{"bug18c", "{{eq . 'P'}}", "true", 'P', true}, {"bug18c", "{{eq . 'P'}}", "true", 'P', true},
{"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, 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 { func zeroArgs() string {

View file

@ -169,6 +169,13 @@ func SetPageInContext(ctx context.Context, p page) context.Context {
return context.WithValue(ctx, texttemplate.PageContextKey, p) 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 { type page interface {
IsNode() bool IsNode() bool
} }

View file

@ -2,6 +2,7 @@ package tplimpl_test
import ( import (
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
@ -160,3 +161,70 @@ title: "S3P1"
b.AssertFileContent("public/s2/p1/index.html", `S2P1`) b.AssertFileContent("public/s2/p1/index.html", `S2P1`)
b.AssertFileContent("public/s3/p1/index.html", `S3P1`) 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()
}