Repository: gohugoio/hugo (language: go)

Issue:
Hugo 0.119 no longer builds - issue with js html template parsing
### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.119.0-b84644c008e0dc2c4b67bd69cccf87a41a03937e+extended linux/amd64 BuildDate=2023-09-24T15:20:17Z VendorInfo=brew
</pre>

### Does this issue reproduce with the latest release?

Yup

### What is the issue?

Since upgrading to 0.119, my site no longer builds. I spent some time deleting things out to come to a reproducible HTML template.

This `list.html` template works with 0.119:

```html
{{ define "main" }}

<script type="module">
  const posts = [];
  const card_template = html`<div>${() => posts.map(post => html``)}</div>`
</script>

<h1>{{ .Title }}</h1>

{{ end }}
```

This `list.html` does not:

```html
{{ define "main" }}

<script type="module">
  const posts = [];
  const card_template = html`<div>${() => posts.map(post => html`<p></p>`)}</div>`
</script>

<h1>{{ .Title }}</h1>

{{ end }}
```

My site builds some reactivity in the front end up using `arrow.js` and Hugo simply populats a big old JSON object to use. It seems like nested js HTML templates are no longer allowed in the validation, which means I'll need to roll back until that's supported. @jmooring directed me to this backend ticket: https://go-review.googlesource.com/c/go/+/507995 


Discussion / hints:
Hey @bep, I noticed this has the upstream tag on it - do you know what upstream project I should raise an issue with and chase it there to try and reduce the number of things to keep track of by the hugo team? :)
This might be resolved in Go 1.22 with https://go-review.googlesource.com/c/go/+/507995 (merged 2 Oct 2023).
Note that we run with a "soft fork" of Go's template packages, so if this is merged upstream and is important enough for us, we could consider pulling this in before Go 1.22 gets released.

Fix patch:
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index e7dae18066e..7d706c7e3d7 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -34,7 +34,6 @@ import (
 	hglob "github.com/gohugoio/hugo/hugofs/glob"
 	"github.com/gohugoio/hugo/modules"
 	"github.com/gohugoio/hugo/parser/metadecoders"
-	"github.com/gohugoio/hugo/tpl"
 	"github.com/spf13/afero"
 )
 
@@ -91,9 +90,6 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
 		return nil, fmt.Errorf("failed to init config: %w", err)
 	}
 
-	// This is unfortunate, but these are global settings.
-	tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl)
-
 	loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning)
 
 	return configs, nil
diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go
index 16f8c23d876..e3bffedcabd 100644
--- a/config/security/securityConfig.go
+++ b/config/security/securityConfig.go
@@ -68,9 +68,6 @@ type Config struct {
 
 	// Allow inline shortcodes
 	EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
-
-	// Go templates related security config.
-	GoTemplates GoTemplates `json:"goTemplates"`
 }
 
 // Exec holds os/exec policies.
@@ -96,15 +93,6 @@ type HTTP struct {
 	MediaTypes Whitelist `json:"mediaTypes"`
 }
 
-type GoTemplates struct {
-
-	// Enable to allow template actions inside bakcticks in ES6 template literals.
-	// This was blocked in Hugo 0.114.0 for security reasons and you now get errors on the form
-	// "... appears in a JS template literal" if you have this in your templates.
-	// See https://github.com/golang/go/issues/59234
-	AllowActionJSTmpl bool
-}
-
 // ToTOML converts c to TOML with [security] as the root.
 func (c Config) ToTOML() string {
 	sec := c.ToSecurityMap()
@@ -127,7 +115,6 @@ func (c Config) CheckAllowedExec(name string) error {
 		}
 	}
 	return nil
-
 }
 
 func (c Config) CheckAllowedGetEnv(name string) error {
@@ -176,7 +163,6 @@ func (c Config) ToSecurityMap() map[string]any {
 		"security": m,
 	}
 	return sec
-
 }
 
 // DecodeConfig creates a privacy Config from a given Hugo configuration.
@@ -206,15 +192,14 @@ func DecodeConfig(cfg config.Provider) (Config, error) {
 	}
 
 	return sc, nil
-
 }
 
 func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
 	return func(
 		f reflect.Type,
 		t reflect.Type,
-		data any) (any, error) {
-
+		data any,
+	) (any, error) {
 		if t != reflect.TypeOf(Whitelist{}) {
 			return data, nil
 		}
@@ -222,7 +207,6 @@ func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
 		wl := types.ToStringSlicePreserveString(data)
 
 		return NewWhitelist(wl...)
-
 	}
 }
 
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index 4ab89a54732..8e14813ec72 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -16,7 +16,7 @@ import (
 )
 
 func main() {
-	// The current is built with 2c1e5b05fe39fc5e6c730dd60e82946b8e67c6ba, tag: go1.21.1.
+	// The current is built with 446a5dcf5a3230ce9832682d8f521071d8a34a2b (go 1.22 dev. Thu Oct 5 12:20:11 2023 -0700)
 	fmt.Println("Forking ...")
 	defer fmt.Println("Done ...")
 
diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go
index 061f17ddbdf..3acb9bdf0e2 100644
--- a/tpl/internal/go_templates/htmltemplate/context.go
+++ b/tpl/internal/go_templates/htmltemplate/context.go
@@ -22,10 +22,15 @@ type context struct {
 	delim   delim
 	urlPart urlPart
 	jsCtx   jsCtx
-	attr    attr
-	element element
-	n       parse.Node // for range break/continue
-	err     *Error
+	// jsBraceDepth contains the current depth, for each JS template literal
+	// string interpolation expression, of braces we've seen. This is used to
+	// determine if the next } will close a JS template literal string
+	// interpolation expression or not.
+	jsBraceDepth []int
+	attr         attr
+	element      element
+	n            parse.Node // for range break/continue
+	err          *Error
 }
 
 func (c context) String() string {
@@ -121,8 +126,8 @@ const (
 	stateJSDqStr
 	// stateJSSqStr occurs inside a JavaScript single quoted string.
 	stateJSSqStr
-	// stateJSBqStr occurs inside a JavaScript back quoted string.
-	stateJSBqStr
+	// stateJSTmplLit occurs inside a JavaScript back quoted string.
+	stateJSTmplLit
 	// stateJSRegexp occurs inside a JavaScript regexp literal.
 	stateJSRegexp
 	// stateJSBlockCmt occurs inside a JavaScript /* block comment */.
@@ -176,14 +181,14 @@ func isInTag(s state) bool {
 }
 
 // isInScriptLiteral returns true if s is one of the literal states within a
-// <script> tag, and as such occurances of "<!--", "<script", and "</script"
+// <script> tag, and as such occurrences of "<!--", "<script", and "</script"
 // need to be treated specially.
 func isInScriptLiteral(s state) bool {
 	// Ignore the comment states (stateJSBlockCmt, stateJSLineCmt,
 	// stateJSHTMLOpenCmt, stateJSHTMLCloseCmt) because their content is already
 	// omitted from the output.
 	switch s {
-	case stateJSDqStr, stateJSSqStr, stateJSBqStr, stateJSRegexp:
+	case stateJSDqStr, stateJSSqStr, stateJSTmplLit, stateJSRegexp:
 		return true
 	}
 	return false
diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go
index a4450a4a983..de4b4de1e53 100644
--- a/tpl/internal/go_templates/htmltemplate/error.go
+++ b/tpl/internal/go_templates/htmltemplate/error.go
@@ -222,6 +222,10 @@ const (
 	// Discussion:
 	//   Package html/template does not support actions inside of JS template
 	//   literals.
+	//
+	// Deprecated: ErrJSTemplate is no longer returned when an action is present
+	// in a JS template literal. Actions inside of JS template literals are now
+	// escaped as expected.
 	ErrJSTemplate
 )
 
diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go
index 5d47a83d352..334bbce0f3a 100644
--- a/tpl/internal/go_templates/htmltemplate/escape.go
+++ b/tpl/internal/go_templates/htmltemplate/escape.go
@@ -8,8 +8,6 @@ import (
 	"bytes"
 	"fmt"
 	"html"
-
-	//"internal/godebug"
 	"io"
 	"regexp"
 
@@ -64,22 +62,23 @@ func evalArgs(args ...any) string {
 
 // funcMap maps command names to functions that render their inputs safe.
 var funcMap = template.FuncMap{
-	"_html_template_attrescaper":     attrEscaper,
-	"_html_template_commentescaper":  commentEscaper,
-	"_html_template_cssescaper":      cssEscaper,
-	"_html_template_cssvaluefilter":  cssValueFilter,
-	"_html_template_htmlnamefilter":  htmlNameFilter,
-	"_html_template_htmlescaper":     htmlEscaper,
-	"_html_template_jsregexpescaper": jsRegexpEscaper,
-	"_html_template_jsstrescaper":    jsStrEscaper,
-	"_html_template_jsvalescaper":    jsValEscaper,
-	"_html_template_nospaceescaper":  htmlNospaceEscaper,
-	"_html_template_rcdataescaper":   rcdataEscaper,
-	"_html_template_srcsetescaper":   srcsetFilterAndEscaper,
-	"_html_template_urlescaper":      urlEscaper,
-	"_html_template_urlfilter":       urlFilter,
-	"_html_template_urlnormalizer":   urlNormalizer,
-	"_eval_args_":                    evalArgs,
+	"_html_template_attrescaper":      attrEscaper,
+	"_html_template_commentescaper":   commentEscaper,
+	"_html_template_cssescaper":       cssEscaper,
+	"_html_template_cssvaluefilter":   cssValueFilter,
+	"_html_template_htmlnamefilter":   htmlNameFilter,
+	"_html_template_htmlescaper":      htmlEscaper,
+	"_html_template_jsregexpescaper":  jsRegexpEscaper,
+	"_html_template_jsstrescaper":     jsStrEscaper,
+	"_html_template_jstmpllitescaper": jsTmplLitEscaper,
+	"_html_template_jsvalescaper":     jsValEscaper,
+	"_html_template_nospaceescaper":   htmlNospaceEscaper,
+	"_html_template_rcdataescaper":    rcdataEscaper,
+	"_html_template_srcsetescaper":    srcsetFilterAndEscaper,
+	"_html_template_urlescaper":       urlEscaper,
+	"_html_template_urlfilter":        urlFilter,
+	"_html_template_urlnormalizer":    urlNormalizer,
+	"_eval_args_":                     evalArgs,
 }
 
 // escaper collects type inferences about templates and changes needed to make
@@ -164,7 +163,6 @@ func (e *escaper) escape(c context, n parse.Node) context {
 	panic("escaping " + n.String() + " is unimplemented")
 }
 
-// Modified by Hugo.
 // var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
 
 // escapeAction escapes an action template node.
@@ -230,16 +228,8 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
 		c.jsCtx = jsCtxDivOp
 	case stateJSDqStr, stateJSSqStr:
 		s = append(s, "_html_template_jsstrescaper")
-	case stateJSBqStr:
-		if SecurityAllowActionJSTmpl.Load() {
-			// debugAllowActionJSTmpl.IncNonDefault()
-			s = append(s, "_html_template_jsstrescaper")
-		} else {
-			return context{
-				state: stateError,
-				err:   errorf(ErrJSTemplate, n, n.Line, "%s appears in a JS template literal", n),
-			}
-		}
+	case stateJSTmplLit:
+		s = append(s, "_html_template_jstmpllitescaper")
 	case stateJSRegexp:
 		s = append(s, "_html_template_jsregexpescaper")
 	case stateCSS:
@@ -398,6 +388,9 @@ var redundantFuncs = map[string]map[string]bool{
 	"_html_template_jsstrescaper": {
 		"_html_template_attrescaper": true,
 	},
+	"_html_template_jstmpllitescaper": {
+		"_html_template_attrescaper": true,
+	},
 	"_html_template_urlescaper": {
 		"_html_template_urlnormalizer": true,
 	},
diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go
index 98e03ad3c46..99edf8f6816 100644
--- a/tpl/internal/go_templates/htmltemplate/hugo_template.go
+++ b/tpl/internal/go_templates/htmltemplate/hugo_template.go
@@ -14,15 +14,9 @@
 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 3b5178eb121..79d9102e565 100644
--- a/tpl/internal/go_templates/htmltemplate/js.go
+++ b/tpl/internal/go_templates/htmltemplate/js.go
@@ -239,6 +239,11 @@ func jsStrEscaper(args ...any) string {
 	return replace(s, jsStrReplacementTable)
 }
 
+func jsTmplLitEscaper(args ...any) string {
+	s, _ := stringify(args...)
+	return replace(s, jsBqStrReplacementTable)
+}
+
 // jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression
 // specials so the result is treated literally when included in a regular
 // expression literal. /foo{{.X}}bar/ matches the string "foo" followed by
@@ -325,6 +330,31 @@ var jsStrReplacementTable = []string{
 	'\\': `\\`,
 }
 
+// jsBqStrReplacementTable is like jsStrReplacementTable except it also contains
+// the special characters for JS template literals: $, {, and }.
+var jsBqStrReplacementTable = []string{
+	0:    `\u0000`,
+	'\t': `\t`,
+	'\n': `\n`,
+	'\v': `\u000b`, // "\v" == "v" on IE 6.
+	'\f': `\f`,
+	'\r': `\r`,
+	// Encode HTML specials as hex so the output can be embedded
+	// in HTML attributes without further encoding.
+	'"':  `\u0022`,
+	'`':  `\u0060`,
+	'&':  `\u0026`,
+	'\'': `\u0027`,
+	'+':  `\u002b`,
+	'/':  `\/`,
+	'<':  `\u003c`,
+	'>':  `\u003e`,
+	'\\': `\\`,
+	'$':  `\u0024`,
+	'{':  `\u007b`,
+	'}':  `\u007d`,
+}
+
 // jsStrNormReplacementTable is like jsStrReplacementTable but does not
 // overencode existing escapes since this table has no entry for `\`.
 var jsStrNormReplacementTable = []string{
@@ -345,6 +375,7 @@ var jsStrNormReplacementTable = []string{
 	'<':  `\u003c`,
 	'>':  `\u003e`,
 }
+
 var jsRegexpReplacementTable = []string{
 	0:    `\u0000`,
 	'\t': `\t`,
diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go
index be7a9205111..eed1e8bcc01 100644
--- a/tpl/internal/go_templates/htmltemplate/state_string.go
+++ b/tpl/internal/go_templates/htmltemplate/state_string.go
@@ -21,7 +21,7 @@ func _() {
 	_ = x[stateJS-10]
 	_ = x[stateJSDqStr-11]
 	_ = x[stateJSSqStr-12]
-	_ = x[stateJSBqStr-13]
+	_ = x[stateJSTmplLit-13]
 	_ = x[stateJSRegexp-14]
 	_ = x[stateJSBlockCmt-15]
 	_ = x[stateJSLineCmt-16]
@@ -39,9 +39,9 @@ func _() {
 	_ = x[stateDead-28]
 }
 
-const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
+const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
 
-var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 214, 233, 241, 254, 267, 280, 293, 304, 320, 335, 345, 354}
+var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 356}
 
 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 432c365d3c3..d5a05f66da4 100644
--- a/tpl/internal/go_templates/htmltemplate/transition.go
+++ b/tpl/internal/go_templates/htmltemplate/transition.go
@@ -27,8 +27,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){
 	stateJS:             tJS,
 	stateJSDqStr:        tJSDelimited,
 	stateJSSqStr:        tJSDelimited,
-	stateJSBqStr:        tJSDelimited,
 	stateJSRegexp:       tJSDelimited,
+	stateJSTmplLit:      tJSTmpl,
 	stateJSBlockCmt:     tBlockCmt,
 	stateJSLineCmt:      tLineCmt,
 	stateJSHTMLOpenCmt:  tLineCmt,
@@ -270,7 +270,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)
@@ -283,7 +283,7 @@ func tJS(c context, s []byte) (context, int) {
 	case '\'':
 		c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
 	case '`':
-		c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp
+		c.state, c.jsCtx = stateJSTmplLit, jsCtxRegexp
 	case '/':
 		switch {
 		case i+1 < len(s) && s[i+1] == '/':
@@ -320,12 +320,66 @@ func tJS(c context, s []byte) (context, int) {
 		if i+1 < len(s) && s[i+1] == '!' {
 			c.state, i = stateJSLineCmt, i+1
 		}
+	case '{':
+		// We only care about tracking brace depth if we are inside of a
+		// template literal.
+		if len(c.jsBraceDepth) == 0 {
+			return c, i + 1
+		}
+		c.jsBraceDepth[len(c.jsBraceDepth)-1]++
+	case '}':
+		if len(c.jsBraceDepth) == 0 {
+			return c, i + 1
+		}
+		// There are no cases where a brace can be escaped in the JS context
+		// that are not syntax errors, it seems. Because of this we can just
+		// count "\}" as "}" and move on, the script is already broken as
+		// fully fledged parsers will just fail anyway.
+		c.jsBraceDepth[len(c.jsBraceDepth)-1]--
+		if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 {
+			return c, i + 1
+		}
+		c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1]
+		c.state = stateJSTmplLit
 	default:
 		panic("unreachable")
 	}
 	return c, i + 1
 }
 
+func tJSTmpl(c context, s []byte) (context, int) {
+	var k int
+	for {
+		i := k + bytes.IndexAny(s[k:], "`\\$")
+		if i < k {
+			break
+		}
+		switch s[i] {
+		case '\\':
+			i++
+			if i == len(s) {
+				return context{
+					state: stateError,
+					err:   errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
+				}, len(s)
+			}
+		case '$':
+			if len(s) >= i+2 && s[i+1] == '{' {
+				c.jsBraceDepth = append(c.jsBraceDepth, 0)
+				c.state = stateJS
+				return c, i + 2
+			}
+		case '`':
+			// end
+			c.state = stateJS
+			return c, i + 1
+		}
+		k = i + 1
+	}
+
+	return c, len(s)
+}
+
 // tJSDelimited is the context transition function for the JS string and regexp
 // states.
 func tJSDelimited(c context, s []byte) (context, int) {
@@ -333,8 +387,6 @@ 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/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go
index 4c01b05ebfe..032784bc3f0 100644
--- a/tpl/internal/go_templates/texttemplate/doc.go
+++ b/tpl/internal/go_templates/texttemplate/doc.go
@@ -438,13 +438,13 @@ produce the text
 By construction, a template may reside in only one association. If it's
 necessary to have a template addressable from multiple associations, the
 template definition must be parsed multiple times to create distinct *Template
-values, or must be copied with the Clone or AddParseTree method.
+values, or must be copied with [Template.Clone] or [Template.AddParseTree].
 
 Parse may be called multiple times to assemble the various associated templates;
-see the ParseFiles and ParseGlob functions and methods for simple ways to parse
-related templates stored in files.
+see [ParseFiles], [ParseGlob], [Template.ParseFiles] and [Template.ParseGlob]
+for simple ways to parse related templates stored in files.
 
-A template may be executed directly or through ExecuteTemplate, which executes
+A template may be executed directly or through [Template.ExecuteTemplate], which executes
 an associated template identified by name. To invoke our example above, we
 might write,
 
diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
index 597866c6866..73153c7648d 100644
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -7,12 +7,13 @@ package template
 import (
 	"errors"
 	"fmt"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 	"io"
 	"reflect"
 	"runtime"
 	"strings"
+
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 )
 
 // maxExecDepth specifies the maximum stack depth of templates within
diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go
index b5a8c9ec50a..a949f896fa3 100644
--- a/tpl/internal/go_templates/texttemplate/funcs.go
+++ b/tpl/internal/go_templates/texttemplate/funcs.go
@@ -478,7 +478,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
 			case k1 == uintKind && k2 == intKind:
 				truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
 			default:
-				if arg1 != zero && arg != zero {
+				if arg1.IsValid() && arg.IsValid() {
 					return false, errBadComparison
 				}
 			}
diff --git a/tpl/template.go b/tpl/template.go
index 70fdf2d3d21..61aaa993f05 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -169,13 +169,6 @@ 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
 }


Test patch:
diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go
index cdfbe63412b..3d58288c9db 100644
--- a/config/security/securityConfig_test.go
+++ b/config/security/securityConfig_test.go
@@ -53,7 +53,6 @@ getEnv=["a", "b"]
 		c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
 		c.Assert(pc.Funcs.Getenv.Accept("a"), qt.IsTrue)
 		c.Assert(pc.Funcs.Getenv.Accept("c"), qt.IsFalse)
-
 	})
 
 	c.Run("String whitelist", func(c *qt.C) {
@@ -80,7 +79,6 @@ osEnv="b"
 		c.Assert(pc.Exec.Allow.Accept("d"), qt.IsFalse)
 		c.Assert(pc.Exec.OsEnv.Accept("b"), qt.IsTrue)
 		c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
-
 	})
 
 	c.Run("Default exec.osEnv", func(c *qt.C) {
@@ -105,7 +103,6 @@ allow="a"
 		c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue)
 		c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue)
 		c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
-
 	})
 
 	c.Run("Enable inline shortcodes, legacy", func(c *qt.C) {
@@ -129,9 +126,7 @@ osEnv="b"
 		pc, err := DecodeConfig(cfg)
 		c.Assert(err, qt.IsNil)
 		c.Assert(pc.EnableInlineShortcodes, qt.IsTrue)
-
 	})
-
 }
 
 func TestToTOML(t *testing.T) {
@@ -140,7 +135,7 @@ func TestToTOML(t *testing.T) {
 	got := DefaultConfig.ToTOML()
 
 	c.Assert(got, qt.Equals,
-		"[security]\n  enableInlineShortcodes = false\n\n  [security.exec]\n    allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n    osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n  [security.funcs]\n    getenv = ['^HUGO_', '^CI$']\n\n  [security.goTemplates]\n    AllowActionJSTmpl = false\n\n  [security.http]\n    methods = ['(?i)GET|POST']\n    urls = ['.*']",
+		"[security]\n  enableInlineShortcodes = false\n\n  [security.exec]\n    allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n    osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n  [security.funcs]\n    getenv = ['^HUGO_', '^CI$']\n\n  [security.http]\n    methods = ['(?i)GET|POST']\n    urls = ['.*']",
 	)
 }
 
@@ -169,5 +164,4 @@ func TestDecodeConfigDefault(t *testing.T) {
 	c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsFalse)
 	c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
 	c.Assert(pc.Exec.OsEnv.Accept("MYSECRET"), qt.IsFalse)
-
 }
diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go
index 064b0910730..fef952fa609 100644
--- a/tpl/internal/go_templates/fmtsort/sort_test.go
+++ b/tpl/internal/go_templates/fmtsort/sort_test.go
@@ -6,13 +6,14 @@ package fmtsort_test
 
 import (
 	"fmt"
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
 	"math"
 	"reflect"
 	"sort"
 	"strings"
 	"testing"
 	"unsafe"
+
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
 )
 
 var compareTests = [][]reflect.Value{
@@ -38,7 +39,7 @@ var compareTests = [][]reflect.Value{
 	ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]),
 	ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}),
 	ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}),
-	ct(reflect.TypeOf(any(any(0))), iFace, 1, 2, 3),
+	ct(reflect.TypeOf(any(0)), iFace, 1, 2, 3),
 }
 
 var iFace any
@@ -190,12 +191,15 @@ func sprintKey(key reflect.Value) string {
 var (
 	ints  [3]int
 	chans = makeChans()
+	// pin   runtime.Pinner
 )
 
 func makeChans() []chan int {
 	cs := []chan int{make(chan int), make(chan int), make(chan int)}
 	// Order channels by address. See issue #49431.
-	// TODO: pin these pointers once pinning is available (#46787).
+	for i := range cs {
+		reflect.ValueOf(cs[i]).UnsafePointer()
+	}
 	sort.Slice(cs, func(i, j int) bool {
 		return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
 	})
diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go
index 4ad5316fb87..b202afde0bc 100644
--- a/tpl/internal/go_templates/htmltemplate/escape_test.go
+++ b/tpl/internal/go_templates/htmltemplate/escape_test.go
@@ -35,14 +35,14 @@ func (x *goodMarshaler) MarshalJSON() ([]byte, error) {
 
 func TestEscape(t *testing.T) {
 	data := struct {
-		F, T    bool
-		C, G, H string
-		A, E    []string
-		B, M    json.Marshaler
-		N       int
-		U       any  // untyped nil
-		Z       *int // typed nil
-		W       htmltemplate.HTML
+		F, T       bool
+		C, G, H, I string
+		A, E       []string
+		B, M       json.Marshaler
+		N          int
+		U          any  // untyped nil
+		Z          *int // typed nil
+		W          htmltemplate.HTML
 	}{
 		F: false,
 		T: true,
@@ -57,6 +57,7 @@ func TestEscape(t *testing.T) {
 		U: nil,
 		Z: nil,
 		W: htmltemplate.HTML(`&iexcl;<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
+		I: "${ asd `` }",
 	}
 	pdata := &data
 
@@ -723,6 +724,21 @@ func TestEscape(t *testing.T) {
 			"<p name=\"{{.U}}\">",
 			"<p name=\"\">",
 		},
+		{
+			"JS template lit special characters",
+			"<script>var a = `{{.I}}`</script>",
+			"<script>var a = `\\u0024\\u007b asd \\u0060\\u0060 \\u007d`</script>",
+		},
+		{
+			"JS template lit special characters, nested lit",
+			"<script>var a = `${ `{{.I}}` }`</script>",
+			"<script>var a = `${ `\\u0024\\u007b asd \\u0060\\u0060 \\u007d` }`</script>",
+		},
+		{
+			"JS template lit, nested JS",
+			"<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>",
+			"<script>var a = `${ var a = \"a \\u0022 d\" }`</script>",
+		},
 	}
 
 	for _, test := range tests {
@@ -981,6 +997,31 @@ func TestErrors(t *testing.T) {
 			"<script>var a = `${a+b}`</script>`",
 			"",
 		},
+		{
+			"<script>var tmpl = `asd`;</script>",
+			``,
+		},
+		{
+			"<script>var tmpl = `${1}`;</script>",
+			``,
+		},
+		{
+			"<script>var tmpl = `${return ``}`;</script>",
+			``,
+		},
+		{
+			"<script>var tmpl = `${return {{.}} }`;</script>",
+			``,
+		},
+		{
+			"<script>var tmpl = `${ let a = {1:1} {{.}} }`;</script>",
+			``,
+		},
+		{
+			"<script>var tmpl = `asd ${return \"{\"}`;</script>",
+			``,
+		},
+
 		// Error cases.
 		{
 			"{{if .Cond}}<a{{end}}",
@@ -1127,10 +1168,6 @@ func TestErrors(t *testing.T) {
 			// html is allowed since it is the last command in the pipeline, but urlquery is not.
 			`predefined escaper "urlquery" disallowed in template`,
 		},
-		{
-			"<script>var tmpl = `asd {{.}}`;</script>",
-			`{{.}} appears in a JS template literal`,
-		},
 	}
 	for _, test := range tests {
 		buf := new(bytes.Buffer)
@@ -1354,7 +1391,7 @@ func TestEscapeText(t *testing.T) {
 		},
 		{
 			"<a onclick=\"`foo",
-			context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript},
+			context{state: stateJSTmplLit, delim: delimDoubleQuote, attr: attrScript},
 		},
 		{
 			`<A ONCLICK="'`,
@@ -1696,6 +1733,94 @@ func TestEscapeText(t *testing.T) {
 			`<svg:a svg:onclick="x()">`,
 			context{},
 		},
+		{
+			"<script>var a = `",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
+		{
+			"<script>var a = `${",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>var a = `${}",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
+		{
+			"<script>var a = `${`",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
+		{
+			"<script>var a = `${var a = \"",
+			context{state: stateJSDqStr, element: elementScript},
+		},
+		{
+			"<script>var a = `${var a = \"`",
+			context{state: stateJSDqStr, element: elementScript},
+		},
+		{
+			"<script>var a = `${var a = \"}",
+			context{state: stateJSDqStr, element: elementScript},
+		},
+		{
+			"<script>var a = `${``",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>var a = `${`}",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
+		{
+			"<script>`${ {} } asd`</script><script>`${ {} }",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
+		{
+			"<script>var foo = `${ (_ => { return \"x\" })() + \"${",
+			context{state: stateJSDqStr, element: elementScript},
+		},
+		{
+			"<script>var a = `${ {</script><script>var b = `${ x }",
+			context{state: stateJSTmplLit, element: elementScript, jsCtx: jsCtxDivOp},
+		},
+		{
+			"<script>var foo = `x` + \"${",
+			context{state: stateJSDqStr, element: elementScript},
+		},
+		{
+			"<script>function f() { var a = `${}`; }",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>{`${}`}",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>`${ function f() { return `${1}` }() }`",
+			context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
+		},
+		{
+			"<script>function f() {`${ function f() { `${1}` } }`}",
+			context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
+		},
+		{
+			"<script>`${ { `` }",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>`${ { }`",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
+		{
+			"<script>var foo = `${ foo({ a: { c: `${",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>var foo = `${ foo({ a: { c: `${ {{.}} }` }, b: ",
+			context{state: stateJS, element: elementScript},
+		},
+		{
+			"<script>`${ `}",
+			context{state: stateJSTmplLit, element: elementScript},
+		},
 	}
 
 	for _, test := range tests {
diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go
index 8f9b893b4ec..e91aeafa157 100644
--- a/tpl/internal/go_templates/htmltemplate/exec_test.go
+++ b/tpl/internal/go_templates/htmltemplate/exec_test.go
@@ -325,12 +325,16 @@ var execTests = []execTest{
 	{"$.U.V", "{{$.U.V}}", "v", tVal, true},
 	{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
 	{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
-	{"nested assignment",
+	{
+		"nested assignment",
 		"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
-		"3", tVal, true},
-	{"nested assignment changes the last declaration",
+		"3", tVal, true,
+	},
+	{
+		"nested assignment changes the last declaration",
 		"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
-		"1", tVal, true},
+		"1", tVal, true,
+	},
 
 	// Type with String method.
 	{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, //  NOTE: -<6666>- in text/template
@@ -377,15 +381,21 @@ var execTests = []execTest{
 	{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true},
 	{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true},
 	{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
-	{"method on chained var",
+	{
+		"method on chained var",
 		"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
-		"true", tVal, true},
-	{"chained method",
+		"true", tVal, true,
+	},
+	{
+		"chained method",
 		"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
-		"true", tVal, true},
-	{"chained method on variable",
+		"true", tVal, true,
+	},
+	{
+		"chained method on variable",
 		"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
-		"true", tVal, true},
+		"true", tVal, true,
+	},
 	{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
 	{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
 	{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -471,10 +481,14 @@ var execTests = []execTest{
 	{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
 
 	// HTML.
-	{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
-		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
-	{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
-		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
+	{
+		"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
+		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
+	},
+	{
+		"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
+		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
+	},
 	{"html", `{{html .PS}}`, "a string", tVal, true},
 	{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
 	{"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template
@@ -838,7 +852,7 @@ var delimPairs = []string{
 
 func TestDelims(t *testing.T) {
 	const hello = "Hello, world"
-	var value = struct{ Str string }{hello}
+	value := struct{ Str string }{hello}
 	for i := 0; i < len(delimPairs); i += 2 {
 		text := ".Str"
 		left := delimPairs[i+0]
@@ -861,7 +875,7 @@ func TestDelims(t *testing.T) {
 		if err != nil {
 			t.Fatalf("delim %q text %q parse err %s", left, text, err)
 		}
-		var b = new(strings.Builder)
+		b := new(strings.Builder)
 		err = tmpl.Execute(b, value)
 		if err != nil {
 			t.Fatalf("delim %q exec err %s", left, err)
@@ -962,7 +976,7 @@ const treeTemplate = `
 `
 
 func TestTree(t *testing.T) {
-	var tree = &Tree{
+	tree := &Tree{
 		1,
 		&Tree{
 			2, &Tree{
@@ -1213,7 +1227,7 @@ var cmpTests = []cmpTest{
 
 func TestComparison(t *testing.T) {
 	b := new(strings.Builder)
-	var cmpStruct = struct {
+	cmpStruct := struct {
 		Uthree, Ufour  uint
 		NegOne, Three  int
 		Ptr, NilPtr    *int
diff --git a/tpl/internal/go_templates/testenv/exec.go b/tpl/internal/go_templates/testenv/exec.go
index 88791b3b460..14460224800 100644
--- a/tpl/internal/go_templates/testenv/exec.go
+++ b/tpl/internal/go_templates/testenv/exec.go
@@ -60,13 +60,6 @@ func tryExec() error {
 	// may as well use the same path so that this branch can be tested without
 	// an ios environment.
 
-	/*if !testing.Testing() {
-		// This isn't a standard 'go test' binary, so we don't know how to
-		// self-exec in a way that should succeed without side effects.
-		// Just forget it.
-		return errors.New("can't probe for exec support with a non-test executable")
-	}*/
-
 	// We know that this is a test executable. We should be able to run it with a
 	// no-op flag to check for overall exec support.
 	exe, err := os.Executable()
@@ -99,11 +92,14 @@ func MustHaveExecPath(t testing.TB, path string) {
 // CleanCmdEnv will fill cmd.Env with the environment, excluding certain
 // variables that could modify the behavior of the Go tools such as
 // GODEBUG and GOTRACEBACK.
+//
+// If the caller wants to set cmd.Dir, set it before calling this function,
+// so PWD will be set correctly in the environment.
 func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
 	if cmd.Env != nil {
 		panic("environment already set")
 	}
-	for _, env := range os.Environ() {
+	for _, env := range cmd.Environ() {
 		// Exclude GODEBUG from the environment to prevent its output
 		// from breaking tests that are trying to parse other command output.
 		if strings.HasPrefix(env, "GODEBUG=") {
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
index 2430ae6cf3b..97d897a1f3c 100644
--- a/tpl/internal/go_templates/testenv/testenv.go
+++ b/tpl/internal/go_templates/testenv/testenv.go
@@ -15,10 +15,6 @@ import (
 	"errors"
 	"flag"
 	"fmt"
-
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
-
-	//"internal/platform"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -27,6 +23,8 @@ import (
 	"strings"
 	"sync"
 	"testing"
+
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
 )
 
 // Save the original environment during init for use in checks. A test
@@ -45,8 +43,8 @@ func Builder() string {
 
 // HasGoBuild reports whether the current system can build programs with “go build”
 // and then run them with os.StartProcess or exec.Command.
+// Modified by Hugo (not needed)
 func HasGoBuild() bool {
-	// Modified by Hugo (not needed)
 	return false
 }
 
@@ -69,13 +67,13 @@ func MustHaveGoBuild(t testing.TB) {
 	}
 }
 
-// HasGoRun reports whether the current system can run programs with “go run.”
+// HasGoRun reports whether the current system can run programs with “go run”.
 func HasGoRun() bool {
 	// For now, having go run and having go build are the same.
 	return HasGoBuild()
 }
 
-// MustHaveGoRun checks that the current system can run programs with “go run.”
+// MustHaveGoRun checks that the current system can run programs with “go run”.
 // If not, MustHaveGoRun calls t.Skip with an explanation.
 func MustHaveGoRun(t testing.TB) {
 	if !HasGoRun() {
@@ -300,8 +298,8 @@ func MustHaveCGO(t testing.TB) {
 
 // CanInternalLink reports whether the current system can link programs with
 // internal linking.
+// Modified by Hugo (not needed)
 func CanInternalLink(withCgo bool) bool {
-	// Modified by Hugo (not needed)
 	return false
 }
 
@@ -320,8 +318,8 @@ func MustInternalLink(t testing.TB, withCgo bool) {
 // MustHaveBuildMode reports whether the current system can build programs in
 // the given build mode.
 // If not, MustHaveBuildMode calls t.Skip with an explanation.
+// Modified by Hugo (not needed)
 func MustHaveBuildMode(t testing.TB, buildmode string) {
-	// Modified by Hugo (not needed)
 }
 
 // HasSymlink reports whether the current system can use os.Symlink.
@@ -438,7 +436,7 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
 		}
 	}
 
-	if err := os.WriteFile(dstPath, icfg.Bytes(), 0666); err != nil {
+	if err := os.WriteFile(dstPath, icfg.Bytes(), 0o666); err != nil {
 		t.Fatal(err)
 	}
 }
diff --git a/tpl/internal/go_templates/testenv/testenv_notunix.go b/tpl/internal/go_templates/testenv/testenv_notunix.go
index 9d55f6258a9..916f1815379 100644
--- a/tpl/internal/go_templates/testenv/testenv_notunix.go
+++ b/tpl/internal/go_templates/testenv/testenv_notunix.go
@@ -7,6 +7,8 @@
 package testenv
 
 import (
+	"errors"
+	"io/fs"
 	"os"
 )
 
@@ -15,6 +17,5 @@ import (
 var Sigquit = os.Kill
 
 func syscallIsNotSupported(err error) bool {
-	// Removed by Hugo (not supported in Go 1.20).
-	return false
+	return errors.Is(err, fs.ErrPermission)
 }
diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go
index 564bff1abdd..13e11abfb53 100644
--- a/tpl/internal/go_templates/testenv/testenv_test.go
+++ b/tpl/internal/go_templates/testenv/testenv_test.go
@@ -5,13 +5,13 @@
 package testenv_test
 
 import (
-	"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
-	//"internal/platform"
 	"os"
 	"path/filepath"
 	"runtime"
 	"strings"
 	"testing"
+
+	"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
 )
 
 func TestGoToolLocation(t *testing.T) {
@@ -83,3 +83,26 @@ func TestMustHaveExec(t *testing.T) {
 		}
 	}
 }
+
+func TestCleanCmdEnvPWD(t *testing.T) {
+	// Test that CleanCmdEnv sets PWD if cmd.Dir is set.
+	switch runtime.GOOS {
+	case "plan9", "windows":
+		t.Skipf("PWD is not used on %s", runtime.GOOS)
+	}
+	dir := t.TempDir()
+	cmd := testenv.Command(t, testenv.GoToolPath(t), "help")
+	cmd.Dir = dir
+	cmd = testenv.CleanCmdEnv(cmd)
+
+	for _, env := range cmd.Env {
+		if strings.HasPrefix(env, "PWD=") {
+			pwd := strings.TrimPrefix(env, "PWD=")
+			if pwd != dir {
+				t.Errorf("unexpected PWD: want %s, got %s", dir, pwd)
+			}
+			return
+		}
+	}
+	t.Error("PWD not set in cmd.Env")
+}
diff --git a/tpl/internal/go_templates/testenv/testenv_unix.go b/tpl/internal/go_templates/testenv/testenv_unix.go
index abb89eeca06..296eefc7a71 100644
--- a/tpl/internal/go_templates/testenv/testenv_unix.go
+++ b/tpl/internal/go_templates/testenv/testenv_unix.go
@@ -7,6 +7,8 @@
 package testenv
 
 import (
+	"errors"
+	"io/fs"
 	"syscall"
 )
 
@@ -15,6 +17,27 @@ import (
 var Sigquit = syscall.SIGQUIT
 
 func syscallIsNotSupported(err error) bool {
-	// Removed by Hugo (not supported in Go 1.20)
+	if err == nil {
+		return false
+	}
+
+	var errno syscall.Errno
+	if errors.As(err, &errno) {
+		switch errno {
+		case syscall.EPERM, syscall.EROFS:
+			// User lacks permission: either the call requires root permission and the
+			// user is not root, or the call is denied by a container security policy.
+			return true
+		case syscall.EINVAL:
+			// Some containers return EINVAL instead of EPERM if a system call is
+			// denied by security policy.
+			return true
+		}
+	}
+
+	if errors.Is(err, fs.ErrPermission) {
+		return true
+	}
+
 	return false
 }
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index b8b3f3bbe19..030e20ca0a4 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -320,12 +320,16 @@ var execTests = []execTest{
 	{"$.U.V", "{{$.U.V}}", "v", tVal, true},
 	{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
 	{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
-	{"nested assignment",
+	{
+		"nested assignment",
 		"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
-		"3", tVal, true},
-	{"nested assignment changes the last declaration",
+		"3", tVal, true,
+	},
+	{
+		"nested assignment changes the last declaration",
 		"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
-		"1", tVal, true},
+		"1", tVal, true,
+	},
 
 	// Type with String method.
 	{"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true},
@@ -372,15 +376,21 @@ var execTests = []execTest{
 	{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true},
 	{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true},
 	{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
-	{"method on chained var",
+	{
+		"method on chained var",
 		"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
-		"true", tVal, true},
-	{"chained method",
+		"true", tVal, true,
+	},
+	{
+		"chained method",
 		"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
-		"true", tVal, true},
-	{"chained method on variable",
+		"true", tVal, true,
+	},
+	{
+		"chained method on variable",
 		"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
-		"true", tVal, true},
+		"true", tVal, true,
+	},
 	{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
 	{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
 	{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -466,10 +476,14 @@ var execTests = []execTest{
 	{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
 
 	// HTML.
-	{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
-		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
-	{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
-		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
+	{
+		"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
+		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
+	},
+	{
+		"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
+		"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
+	},
 	{"html", `{{html .PS}}`, "a string", tVal, true},
 	{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
 	{"html untyped nil", `{{html .Empty0}}`, "&lt;no value&gt;", tVal, true},
@@ -844,7 +858,7 @@ var delimPairs = []string{
 
 func TestDelims(t *testing.T) {
 	const hello = "Hello, world"
-	var value = struct{ Str string }{hello}
+	value := struct{ Str string }{hello}
 	for i := 0; i < len(delimPairs); i += 2 {
 		text := ".Str"
 		left := delimPairs[i+0]
@@ -867,7 +881,7 @@ func TestDelims(t *testing.T) {
 		if err != nil {
 			t.Fatalf("delim %q text %q parse err %s", left, text, err)
 		}
-		var b = new(strings.Builder)
+		b := new(strings.Builder)
 		err = tmpl.Execute(b, value)
 		if err != nil {
 			t.Fatalf("delim %q exec err %s", left, err)
@@ -990,7 +1004,7 @@ const treeTemplate = `
 `
 
 func TestTree(t *testing.T) {
-	var tree = &Tree{
+	tree := &Tree{
 		1,
 		&Tree{
 			2, &Tree{
@@ -1243,7 +1257,7 @@ var cmpTests = []cmpTest{
 
 func TestComparison(t *testing.T) {
 	b := new(strings.Builder)
-	var cmpStruct = struct {
+	cmpStruct := struct {
 		Uthree, Ufour    uint
 		NegOne, Three    int
 		Ptr, NilPtr      *int
diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go
index fa511fbab35..1b2cffce679 100644
--- a/tpl/tplimpl/integration_test.go
+++ b/tpl/tplimpl/integration_test.go
@@ -114,7 +114,6 @@ and2: true
 or2: true
 counter2: 3
 `)
-
 }
 
 // Issue 10495
@@ -163,7 +162,6 @@ title: "S3P1"
 }
 
 func TestGoTemplateBugs(t *testing.T) {
-
 	t.Run("Issue 11112", func(t *testing.T) {
 		t.Parallel()
 
@@ -188,11 +186,9 @@ func TestGoTemplateBugs(t *testing.T) {
 
 		b.AssertFileContent("public/index.html", `key = value`)
 	})
-
 }
 
 func TestSecurityAllowActionJSTmpl(t *testing.T) {
-
 	filesTemplate := `
 -- config.toml --
 SECURITYCONFIG
@@ -211,20 +207,6 @@ var a = §§{{.Title }}§§;
 		},
 	).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()
-
+	// This used to fail, but not in >= Hugo 0.121.0.
+	b.Assert(err, qt.IsNil)
 }