Repository: gohugoio/hugo (language: go)

Issue:
In nested range the map key and value become are inverted.
<!-- Please answer these questions before submitting your issue. Thanks! -->

I upgraded my hugo from v0.99 to v0.113 yesterday, I get a strange output from `range $key, $value = range .` , the value of  **$key** is output as **$value**, and the value of **$value** is output as **$key**.

The full source code from [https://github.com/fastbootstrap/atlassian-design-for-bootstrap/blob/main/site/layouts/shortcodes/class-api.html](https://github.com/fastbootstrap/atlassian-design-for-bootstrap/blob/main/site/layouts/shortcodes/class-api.html)

```html
      {{ range index .Site.Data.css $module }}
        {{ $properties := .property }}
        {{ $values := .values }}
        {{ $prefix := .class | default .property }}
        {{ $format := .format | default "%s-%s" }}
        {{ range $values }}
          {{ $key := . }}
          {{ $value := . }}
          {{ if reflect.IsMap . }}
            {{ range $key, $value = . }}  **WARNING: here, key and value is inverted **
            {{ end }}
          {{ end }}
          {{ $class :=trim (printf $format $prefix (string $key)) "-" }}
          {{ $styles := slice }}
          {{ range split $properties " " }}
            {{ $property := trim . "" }}
            {{ $styles = $styles | append ( printf "%s: %s;" $property (string $value)) }}
          {{ end }}
          <tr>
            <td class="text-blue-400 text-nowrap"><code>{{ printf ".%s" $class }}</code></td>
            <td class="text-purple-300">{{ delimit $styles "<br/>" }}</td>           
          </tr>
        {{ end }}
      {{ end }}
```

The warning output from `{{ range $key, $value = . }}`
```
          {{ if reflect.IsMap . }}
            {{ range $key, $value = . }}  **WARNING: here, key and value are inverted **
            {{ end }}
          {{ end }}
```

The normal output from Hugo v0.99

| Class | Property |
| ------------- |:-------------:|
|.border-0	| border-width: 0; |
|.border-1	| border-width: 1px; |
|.border-2	| border-width: 2px; |
|.border-3	| border-width: 3px; |
|.border-4	| border-width: 4px; |
|.border-5	| border-width: 5px; |

Since upgrade to Hugo v0.113, The output value key become value, value become key.

| Class        | Property           |
| ------------- |:-------------:|
| .border-0	| border-width: 0; |
| .border-1px	| border-width: 1; |
| .border-2px	| border-width: 2; |
| .border-3px	| border-width: 3; |
| .border-4px	| border-width: 4; |
| .border-5px	| border-width: 5; |


Data from [https://github.com/fastbootstrap/atlassian-design-for-bootstrap/blob/main/site/data/css.yml](https://github.com/fastbootstrap/atlassian-design-for-bootstrap/blob/main/site/data/css.yml)

```yml
border-width:
  - property: border-width
    class: border
    values:
      - 0
      - 1: 1px
      - 2: 2px
      - 3: 3px
      - 4: 4px
      - 5: 5px
```

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.113.0-085c1b3d614e23d218ebf9daad909deaa2390c9a+extended windows/amd64 BuildDate=2023-06-05T15:04:51Z VendorInfo=gohugoio
</pre>

### Does this issue reproduce with the latest release?

Yes



Discussion / hints:
I have not looked closely at your templates, but this issue rings a bell:

https://github.com/golang/go/issues/56490
Verified. The problem was introduced in [v0.106.0](https://github.com/gohugoio/hugo/releases/tag/v0.106.0), where we merged the changes from https://github.com/golang/go/commit/be7068fb0804f661515c678bee9224b90b32869a. So, upstream fixed one thing but broke another.
To reproduce:

```
{{ $m := dict "key" "value" }}
{{ $k := "" }}
{{ $v := "" }}
{{ range $k, $v = $m }}
  {{ $k }} = {{ $v }}    --> "value = key"
{{ end }}

OR 

{{ $s := slice "value" }}
{{ $k := "" }}
{{ $v := "" }}
{{ range $k, $v = $s }}
  {{ $k }} = {{ $v }}    --> "value = 0"
{{ end }}
```
Upstream: 
- <https://github.com/golang/go/issues/60801> 
- <https://go-review.googlesource.com/c/go/+/503575>.
 I'm guessing they will merge that one fast, so I will hold on with any release until I can get that one into Hugo.

Fix patch:
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index ad090d60d1a..eca9d06dfa9 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -34,6 +34,7 @@ import (
 	hglob "github.com/gohugoio/hugo/hugofs/glob"
 	"github.com/gohugoio/hugo/modules"
 	"github.com/gohugoio/hugo/parser/metadecoders"
+	"github.com/gohugoio/hugo/tpl"
 	"github.com/spf13/afero"
 )
 
@@ -89,6 +90,9 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
 		return nil, fmt.Errorf("failed to init config: %w", err)
 	}
 
+	// This is unfortunate, but this is a global setting.
+	tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl)
+
 	return configs, nil
 
 }
diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go
index 8bd12af4b77..5d0db2fb940 100644
--- a/config/security/securityConfig.go
+++ b/config/security/securityConfig.go
@@ -68,6 +68,9 @@ type Config struct {
 
 	// Allow inline shortcodes
 	EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
+
+	// Go templates related security config.
+	GoTemplates GoTemplates `json:"goTemplates"`
 }
 
 // Exec holds os/exec policies.
@@ -93,6 +96,15 @@ type HTTP struct {
 	MediaTypes Whitelist `json:"mediaTypes"`
 }
 
+type GoTemplates struct {
+
+	// Enable to allow template actions inside bakcticks in ES6 template literals.
+	// This was blocked in Hugo 0.114.0 for security reasons and you now get errors on the form
+	// "... appears in a JS template literal" if you have this in your templates.
+	// See https://github.com/golang/go/issues/59234
+	AllowActionJSTmpl bool
+}
+
 // ToTOML converts c to TOML with [security] as the root.
 func (c Config) ToTOML() string {
 	sec := c.ToSecurityMap()
diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go
index 146a95d035c..9f592b57f5a 100644
--- a/tpl/internal/go_templates/htmltemplate/context.go
+++ b/tpl/internal/go_templates/htmltemplate/context.go
@@ -121,6 +121,8 @@ const (
 	stateJSDqStr
 	// stateJSSqStr occurs inside a JavaScript single quoted string.
 	stateJSSqStr
+	// stateJSBqStr occurs inside a JavaScript back quoted string.
+	stateJSBqStr
 	// stateJSRegexp occurs inside a JavaScript regexp literal.
 	stateJSRegexp
 	// stateJSBlockCmt occurs inside a JavaScript /* block comment */.
diff --git a/tpl/internal/go_templates/htmltemplate/css.go b/tpl/internal/go_templates/htmltemplate/css.go
index 890a0c6b227..f650d8b3e84 100644
--- a/tpl/internal/go_templates/htmltemplate/css.go
+++ b/tpl/internal/go_templates/htmltemplate/css.go
@@ -238,7 +238,7 @@ func cssValueFilter(args ...any) string {
 	// inside a string that might embed JavaScript source.
 	for i, c := range b {
 		switch c {
-		case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}':
+		case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}', '<', '>':
 			return filterFailsafe
 		case '-':
 			// Disallow <!-- or -->.
diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go
index 8422b49214b..98b5658f402 100644
--- a/tpl/internal/go_templates/htmltemplate/doc.go
+++ b/tpl/internal/go_templates/htmltemplate/doc.go
@@ -231,5 +231,12 @@ Least Surprise Property:
 "A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who
 knows that contextual autoescaping happens should be able to look at a {{.}}
 and correctly infer what sanitization happens."
+
+As a consequence of the Least Surprise Property, template actions within an
+ECMAScript 6 template literal are disabled by default.
+Handling string interpolation within these literals is rather complex resulting
+in no clear safe way to support it.
+To re-enable template actions within ECMAScript 6 template literals, use the
+GODEBUG=jstmpllitinterp=1 environment variable.
 */
 package template
diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go
index 916b41a82c5..0a62563cf4f 100644
--- a/tpl/internal/go_templates/htmltemplate/error.go
+++ b/tpl/internal/go_templates/htmltemplate/error.go
@@ -215,6 +215,19 @@ const (
 	//   pipeline occurs in an unquoted attribute value context, "html" is
 	//   disallowed. Avoid using "html" and "urlquery" entirely in new templates.
 	ErrPredefinedEscaper
+
+	// errJSTmplLit: "... appears in a JS template literal"
+	// Example:
+	//     <script>var tmpl = `{{.Interp}`</script>
+	// Discussion:
+	//   Package html/template does not support actions inside of JS template
+	//   literals.
+	//
+	// TODO(rolandshoemaker): we cannot add this as an exported error in a minor
+	// release, since it is backwards incompatible with the other minor
+	// releases. As such we need to leave it unexported, and then we'll add it
+	// in the next major release.
+	errJSTmplLit
 )
 
 func (e *Error) Error() string {
diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go
index 3aac865ef1b..ba9b4bbb8db 100644
--- a/tpl/internal/go_templates/htmltemplate/escape.go
+++ b/tpl/internal/go_templates/htmltemplate/escape.go
@@ -161,6 +161,8 @@ func (e *escaper) escape(c context, n parse.Node) context {
 	panic("escaping " + n.String() + " is unimplemented")
 }
 
+// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
+
 // escapeAction escapes an action template node.
 func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
 	if len(n.Pipe.Decl) != 0 {
@@ -224,6 +226,15 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
 		c.jsCtx = jsCtxDivOp
 	case stateJSDqStr, stateJSSqStr:
 		s = append(s, "_html_template_jsstrescaper")
+	case stateJSBqStr:
+		if SecurityAllowActionJSTmpl.Load() { // .Value() == "1" {
+			s = append(s, "_html_template_jsstrescaper")
+		} else {
+			return context{
+				state: stateError,
+				err:   errorf(errJSTmplLit, n, n.Line, "%s appears in a JS template literal", n),
+			}
+		}
 	case stateJSRegexp:
 		s = append(s, "_html_template_jsregexpescaper")
 	case stateCSS:
@@ -370,9 +381,8 @@ func normalizeEscFn(e string) string {
 // for all x.
 var redundantFuncs = map[string]map[string]bool{
 	"_html_template_commentescaper": {
-		"_html_template_attrescaper":    true,
-		"_html_template_nospaceescaper": true,
-		"_html_template_htmlescaper":    true,
+		"_html_template_attrescaper": true,
+		"_html_template_htmlescaper": true,
 	},
 	"_html_template_cssescaper": {
 		"_html_template_attrescaper": true,
diff --git a/tpl/internal/go_templates/htmltemplate/html.go b/tpl/internal/go_templates/htmltemplate/html.go
index bcca0b51a0e..a181699a5bd 100644
--- a/tpl/internal/go_templates/htmltemplate/html.go
+++ b/tpl/internal/go_templates/htmltemplate/html.go
@@ -14,6 +14,9 @@ import (
 // htmlNospaceEscaper escapes for inclusion in unquoted attribute values.
 func htmlNospaceEscaper(args ...any) string {
 	s, t := stringify(args...)
+	if s == "" {
+		return filterFailsafe
+	}
 	if t == contentTypeHTML {
 		return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false)
 	}
diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go
index 99edf8f6816..98e03ad3c46 100644
--- a/tpl/internal/go_templates/htmltemplate/hugo_template.go
+++ b/tpl/internal/go_templates/htmltemplate/hugo_template.go
@@ -14,9 +14,15 @@
 package template
 
 import (
+	"sync/atomic"
+
 	template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 )
 
+// See https://github.com/golang/go/issues/59234
+// Moved here to avoid dependency on Go's internal/debug package.
+var SecurityAllowActionJSTmpl atomic.Bool
+
 /*
 
 This files contains the Hugo related addons. All the other files in this
diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go
index 6187dc03634..3b5178eb121 100644
--- a/tpl/internal/go_templates/htmltemplate/js.go
+++ b/tpl/internal/go_templates/htmltemplate/js.go
@@ -14,6 +14,11 @@ import (
 	"unicode/utf8"
 )
 
+// jsWhitespace contains all of the JS whitespace characters, as defined
+// by the \s character class.
+// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes.
+const jsWhitespace = "\f\n\r\t\v\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
+
 // nextJSCtx returns the context that determines whether a slash after the
 // given run of tokens starts a regular expression instead of a division
 // operator: / or /=.
@@ -27,7 +32,8 @@ import (
 // JavaScript 2.0 lexical grammar and requires one token of lookbehind:
 // https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html
 func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
-	s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029")
+	// Trim all JS whitespace characters
+	s = bytes.TrimRight(s, jsWhitespace)
 	if len(s) == 0 {
 		return preceding
 	}
@@ -309,6 +315,7 @@ var jsStrReplacementTable = []string{
 	// Encode HTML specials as hex so the output can be embedded
 	// in HTML attributes without further encoding.
 	'"':  `\u0022`,
+	'`':  `\u0060`,
 	'&':  `\u0026`,
 	'\'': `\u0027`,
 	'+':  `\u002b`,
@@ -332,6 +339,7 @@ var jsStrNormReplacementTable = []string{
 	'"':  `\u0022`,
 	'&':  `\u0026`,
 	'\'': `\u0027`,
+	'`':  `\u0060`,
 	'+':  `\u002b`,
 	'/':  `\/`,
 	'<':  `\u003c`,
diff --git a/tpl/internal/go_templates/htmltemplate/jsctx_string.go b/tpl/internal/go_templates/htmltemplate/jsctx_string.go
index dd1d87ee454..23948934c95 100644
--- a/tpl/internal/go_templates/htmltemplate/jsctx_string.go
+++ b/tpl/internal/go_templates/htmltemplate/jsctx_string.go
@@ -4,6 +4,15 @@ package template
 
 import "strconv"
 
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[jsCtxRegexp-0]
+	_ = x[jsCtxDivOp-1]
+	_ = x[jsCtxUnknown-2]
+}
+
 const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
 
 var _jsCtx_index = [...]uint8{0, 11, 21, 33}
diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go
index 05104be89c2..6fb1a6eeb0e 100644
--- a/tpl/internal/go_templates/htmltemplate/state_string.go
+++ b/tpl/internal/go_templates/htmltemplate/state_string.go
@@ -4,9 +4,42 @@ package template
 
 import "strconv"
 
-const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateError"
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[stateText-0]
+	_ = x[stateTag-1]
+	_ = x[stateAttrName-2]
+	_ = x[stateAfterName-3]
+	_ = x[stateBeforeValue-4]
+	_ = x[stateHTMLCmt-5]
+	_ = x[stateRCDATA-6]
+	_ = x[stateAttr-7]
+	_ = x[stateURL-8]
+	_ = x[stateSrcset-9]
+	_ = x[stateJS-10]
+	_ = x[stateJSDqStr-11]
+	_ = x[stateJSSqStr-12]
+	_ = x[stateJSBqStr-13]
+	_ = x[stateJSRegexp-14]
+	_ = x[stateJSBlockCmt-15]
+	_ = x[stateJSLineCmt-16]
+	_ = x[stateCSS-17]
+	_ = x[stateCSSDqStr-18]
+	_ = x[stateCSSSqStr-19]
+	_ = x[stateCSSDqURL-20]
+	_ = x[stateCSSSqURL-21]
+	_ = x[stateCSSURL-22]
+	_ = x[stateCSSBlockCmt-23]
+	_ = x[stateCSSLineCmt-24]
+	_ = x[stateError-25]
+	_ = x[stateDead-26]
+}
+
+const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
 
-var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 155, 170, 184, 192, 205, 218, 231, 244, 255, 271, 286, 296}
+var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 204, 217, 230, 243, 256, 267, 283, 298, 308, 317}
 
 func (i state) String() string {
 	if i >= state(len(_state_index)-1) {
diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go
index 06df679330d..92eb3519063 100644
--- a/tpl/internal/go_templates/htmltemplate/transition.go
+++ b/tpl/internal/go_templates/htmltemplate/transition.go
@@ -27,6 +27,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){
 	stateJS:          tJS,
 	stateJSDqStr:     tJSDelimited,
 	stateJSSqStr:     tJSDelimited,
+	stateJSBqStr:     tJSDelimited,
 	stateJSRegexp:    tJSDelimited,
 	stateJSBlockCmt:  tBlockCmt,
 	stateJSLineCmt:   tLineCmt,
@@ -262,7 +263,7 @@ func tURL(c context, s []byte) (context, int) {
 
 // tJS is the context transition function for the JS state.
 func tJS(c context, s []byte) (context, int) {
-	i := bytes.IndexAny(s, `"'/`)
+	i := bytes.IndexAny(s, "\"`'/")
 	if i == -1 {
 		// Entire input is non string, comment, regexp tokens.
 		c.jsCtx = nextJSCtx(s, c.jsCtx)
@@ -274,6 +275,8 @@ func tJS(c context, s []byte) (context, int) {
 		c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
 	case '\'':
 		c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
+	case '`':
+		c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp
 	case '/':
 		switch {
 		case i+1 < len(s) && s[i+1] == '/':
@@ -303,6 +306,8 @@ func tJSDelimited(c context, s []byte) (context, int) {
 	switch c.state {
 	case stateJSSqStr:
 		specials = `\'`
+	case stateJSBqStr:
+		specials = "`\\"
 	case stateJSRegexp:
 		specials = `\/[]`
 	}
diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go
index 62b2e519ea0..597866c6866 100644
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -361,19 +361,27 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
 	// mark top of stack before any variables in the body are pushed.
 	mark := s.mark()
 	oneIteration := func(index, elem reflect.Value) {
-		// Set top var (lexically the second if there are two) to the element.
 		if len(r.Pipe.Decl) > 0 {
 			if r.Pipe.IsAssign {
-				s.setVar(r.Pipe.Decl[0].Ident[0], elem)
+				// With two variables, index comes first.
+				// With one, we use the element.
+				if len(r.Pipe.Decl) > 1 {
+					s.setVar(r.Pipe.Decl[0].Ident[0], index)
+				} else {
+					s.setVar(r.Pipe.Decl[0].Ident[0], elem)
+				}
 			} else {
+				// Set top var (lexically the second if there
+				// are two) to the element.
 				s.setTopVar(1, elem)
 			}
 		}
-		// Set next var (lexically the first if there are two) to the index.
 		if len(r.Pipe.Decl) > 1 {
 			if r.Pipe.IsAssign {
-				s.setVar(r.Pipe.Decl[1].Ident[0], index)
+				s.setVar(r.Pipe.Decl[1].Ident[0], elem)
 			} else {
+				// Set next var (lexically the first if there
+				// are two) to the index.
 				s.setTopVar(2, index)
 			}
 		}
diff --git a/tpl/template.go b/tpl/template.go
index 7a793101c51..91a482c2504 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -169,6 +169,13 @@ func SetPageInContext(ctx context.Context, p page) context.Context {
 	return context.WithValue(ctx, texttemplate.PageContextKey, p)
 }
 
+// SetSecurityAllowActionJSTmpl sets the global setting for allowing tempalte actions in JS template literals.
+// This was added in Hugo 0.114.0.
+// See https://github.com/golang/go/issues/59234
+func SetSecurityAllowActionJSTmpl(b bool) {
+	htmltemplate.SecurityAllowActionJSTmpl.Store(b)
+}
+
 type page interface {
 	IsNode() bool
 }


Test patch:
diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go
index 3bfd59ce33b..12ce3aae449 100644
--- a/config/security/securityConfig_test.go
+++ b/config/security/securityConfig_test.go
@@ -140,7 +140,7 @@ func TestToTOML(t *testing.T) {
 	got := DefaultConfig.ToTOML()
 
 	c.Assert(got, qt.Equals,
-		"[security]\n  enableInlineShortcodes = false\n\n  [security.exec]\n    allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n    osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n  [security.funcs]\n    getenv = ['^HUGO_', '^CI$']\n\n  [security.http]\n    methods = ['(?i)GET|POST']\n    urls = ['.*']",
+		"[security]\n  enableInlineShortcodes = false\n\n  [security.exec]\n    allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n    osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n  [security.funcs]\n    getenv = ['^HUGO_', '^CI$']\n\n  [security.goTemplates]\n    AllowActionJSTmpl = false\n\n  [security.http]\n    methods = ['(?i)GET|POST']\n    urls = ['.*']",
 	)
 }
 
diff --git a/tpl/internal/go_templates/htmltemplate/css_test.go b/tpl/internal/go_templates/htmltemplate/css_test.go
index 7d8ad8b5989..f44568930dd 100644
--- a/tpl/internal/go_templates/htmltemplate/css_test.go
+++ b/tpl/internal/go_templates/htmltemplate/css_test.go
@@ -234,6 +234,8 @@ func TestCSSValueFilter(t *testing.T) {
 		{`-exp\000052 ession(alert(1337))`, "ZgotmplZ"},
 		{`-expre\0000073sion`, "-expre\x073sion"},
 		{`@import url evil.css`, "ZgotmplZ"},
+		{"<", "ZgotmplZ"},
+		{">", "ZgotmplZ"},
 	}
 	for _, test := range tests {
 		got := cssValueFilter(test.css)
diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go
index a08ea57ef70..680ba6fa714 100644
--- a/tpl/internal/go_templates/htmltemplate/escape_test.go
+++ b/tpl/internal/go_templates/htmltemplate/escape_test.go
@@ -683,38 +683,49 @@ func TestEscape(t *testing.T) {
 			`<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`,
 			`<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`,
 		},
+		{
+			"unquoted empty attribute value (plaintext)",
+			"<p name={{.U}}>",
+			"<p name=ZgotmplZ>",
+		},
+		{
+			"unquoted empty attribute value (url)",
+			"<p href={{.U}}>",
+			"<p href=ZgotmplZ>",
+		},
+		{
+			"quoted empty attribute value",
+			"<p name=\"{{.U}}\">",
+			"<p name=\"\">",
+		},
 	}
 
 	for _, test := range tests {
-		tmpl := New(test.name)
-		tmpl = Must(tmpl.Parse(test.input))
-		// Check for bug 6459: Tree field was not set in Parse.
-		if tmpl.Tree != tmpl.text.Tree {
-			t.Errorf("%s: tree not set properly", test.name)
-			continue
-		}
-		b := new(strings.Builder)
-		if err := tmpl.Execute(b, data); err != nil {
-			t.Errorf("%s: template execution failed: %s", test.name, err)
-			continue
-		}
-		if w, g := test.output, b.String(); w != g {
-			t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
-			continue
-		}
-		b.Reset()
-		if err := tmpl.Execute(b, pdata); err != nil {
-			t.Errorf("%s: template execution failed for pointer: %s", test.name, err)
-			continue
-		}
-		if w, g := test.output, b.String(); w != g {
-			t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
-			continue
-		}
-		if tmpl.Tree != tmpl.text.Tree {
-			t.Errorf("%s: tree mismatch", test.name)
-			continue
-		}
+		t.Run(test.name, func(t *testing.T) {
+			tmpl := New(test.name)
+			tmpl = Must(tmpl.Parse(test.input))
+			// Check for bug 6459: Tree field was not set in Parse.
+			if tmpl.Tree != tmpl.text.Tree {
+				t.Fatalf("%s: tree not set properly", test.name)
+			}
+			b := new(strings.Builder)
+			if err := tmpl.Execute(b, data); err != nil {
+				t.Fatalf("%s: template execution failed: %s", test.name, err)
+			}
+			if w, g := test.output, b.String(); w != g {
+				t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
+			}
+			b.Reset()
+			if err := tmpl.Execute(b, pdata); err != nil {
+				t.Fatalf("%s: template execution failed for pointer: %s", test.name, err)
+			}
+			if w, g := test.output, b.String(); w != g {
+				t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
+			}
+			if tmpl.Tree != tmpl.text.Tree {
+				t.Fatalf("%s: tree mismatch", test.name)
+			}
+		})
 	}
 }
 
@@ -941,6 +952,10 @@ func TestErrors(t *testing.T) {
 			"{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
 			"",
 		},
+		{
+			"<script>var a = `${a+b}`</script>`",
+			"",
+		},
 		// Error cases.
 		{
 			"{{if .Cond}}<a{{end}}",
@@ -1087,6 +1102,10 @@ func TestErrors(t *testing.T) {
 			// html is allowed since it is the last command in the pipeline, but urlquery is not.
 			`predefined escaper "urlquery" disallowed in template`,
 		},
+		{
+			"<script>var tmpl = `asd {{.}}`;</script>",
+			`{{.}} appears in a JS template literal`,
+		},
 	}
 	for _, test := range tests {
 		buf := new(bytes.Buffer)
@@ -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},
diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go
index 483a3694f5d..67a9213370b 100644
--- a/tpl/internal/go_templates/htmltemplate/js_test.go
+++ b/tpl/internal/go_templates/htmltemplate/js_test.go
@@ -83,14 +83,17 @@ func TestNextJsCtx(t *testing.T) {
 		{jsCtxDivOp, "0"},
 		// Dots that are part of a number are div preceders.
 		{jsCtxDivOp, "0."},
+		// Some JS interpreters treat NBSP as a normal space, so
+		// we must too in order to properly escape things.
+		{jsCtxRegexp, "=\u00A0"},
 	}
 
 	for _, test := range tests {
-		if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx {
-			t.Errorf("want %s got %q", test.jsCtx, test.s)
+		if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx {
+			t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
 		}
-		if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx {
-			t.Errorf("want %s got %q", test.jsCtx, test.s)
+		if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx {
+			t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
 		}
 	}
 
@@ -294,7 +297,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
 				`0123456789:;\u003c=\u003e?` +
 				`@ABCDEFGHIJKLMNO` +
 				`PQRSTUVWXYZ[\\]^_` +
-				"`abcdefghijklmno" +
+				"\\u0060abcdefghijklmno" +
 				"pqrstuvwxyz{|}~\u007f" +
 				"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
 		},
diff --git a/tpl/internal/go_templates/testenv/exec.go b/tpl/internal/go_templates/testenv/exec.go
index ca4023647d4..13b4c7102e7 100644
--- a/tpl/internal/go_templates/testenv/exec.go
+++ b/tpl/internal/go_templates/testenv/exec.go
@@ -9,11 +9,9 @@ import (
 	"os"
 	"os/exec"
 	"runtime"
-	"strconv"
 	"strings"
 	"sync"
 	"testing"
-	"time"
 )
 
 // HasExec reports whether the current system can start new processes
@@ -84,87 +82,7 @@ func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
 //   - fails the test if the command does not complete before the test's deadline, and
 //   - sets a Cleanup function that verifies that the test did not leak a subprocess.
 func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd {
-	t.Helper()
-	MustHaveExec(t)
-
-	var (
-		cancelCtx   context.CancelFunc
-		gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging)
-	)
-
-	if t, ok := t.(interface {
-		testing.TB
-		Deadline() (time.Time, bool)
-	}); ok {
-		if td, ok := t.Deadline(); ok {
-			// Start with a minimum grace period, just long enough to consume the
-			// output of a reasonable program after it terminates.
-			gracePeriod = 100 * time.Millisecond
-			if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
-				scale, err := strconv.Atoi(s)
-				if err != nil {
-					t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err)
-				}
-				gracePeriod *= time.Duration(scale)
-			}
-
-			// If time allows, increase the termination grace period to 5% of the
-			// test's remaining time.
-			testTimeout := time.Until(td)
-			if gp := testTimeout / 20; gp > gracePeriod {
-				gracePeriod = gp
-			}
-
-			// When we run commands that execute subprocesses, we want to reserve two
-			// grace periods to clean up: one for the delay between the first
-			// termination signal being sent (via the Cancel callback when the Context
-			// expires) and the process being forcibly terminated (via the WaitDelay
-			// field), and a second one for the delay becween the process being
-			// terminated and and the test logging its output for debugging.
-			//
-			// (We want to ensure that the test process itself has enough time to
-			// log the output before it is also terminated.)
-			cmdTimeout := testTimeout - 2*gracePeriod
-
-			if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout {
-				// Either ctx doesn't have a deadline, or its deadline would expire
-				// after (or too close before) the test has already timed out.
-				// Add a shorter timeout so that the test will produce useful output.
-				ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout)
-			}
-		}
-	}
-
-	cmd := exec.CommandContext(ctx, name, args...)
-	/*cmd.Cancel = func() error {
-		if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded {
-			// The command timed out due to running too close to the test's deadline.
-			// There is no way the test did that intentionally — it's too close to the
-			// wire! — so mark it as a test failure. That way, if the test expects the
-			// command to fail for some other reason, it doesn't have to distinguish
-			// between that reason and a timeout.
-			t.Errorf("test timed out while running command: %v", cmd)
-		} else {
-			// The command is being terminated due to ctx being canceled, but
-			// apparently not due to an explicit test deadline that we added.
-			// Log that information in case it is useful for diagnosing a failure,
-			// but don't actually fail the test because of it.
-			t.Logf("%v: terminating command: %v", ctx.Err(), cmd)
-		}
-		return cmd.Process.Signal(Sigquit)
-	}
-	cmd.WaitDelay = gracePeriod*/
-
-	t.Cleanup(func() {
-		if cancelCtx != nil {
-			cancelCtx()
-		}
-		if cmd.Process != nil && cmd.ProcessState == nil {
-			t.Errorf("command was started, but test did not wait for it to complete: %v", cmd)
-		}
-	})
-
-	return cmd
+	panic("Not implemented, Hugo is not using this")
 }
 
 // Command is like exec.Command, but applies the same changes as
diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go
index 6bfa54a979c..91de6e76cbb 100644
--- a/tpl/internal/go_templates/testenv/testenv.go
+++ b/tpl/internal/go_templates/testenv/testenv.go
@@ -258,7 +258,7 @@ func MustHaveCGO(t testing.TB) {
 // CanInternalLink reports whether the current system can link programs with
 // internal linking.
 func CanInternalLink() bool {
-	return false
+	panic("not implemented, not needed by Hugo")
 }
 
 // MustInternalLink checks that the current system can link programs with internal
@@ -349,15 +349,5 @@ func SkipIfOptimizationOff(t testing.TB) {
 // dstPath containing entries for the packages in std and cmd in addition
 // to the package to package file mappings in additionalPackageFiles.
 func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) {
-	/*importcfg, err := goroot.Importcfg()
-	for k, v := range additionalPackageFiles {
-		importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v)
-	}
-	if err != nil {
-		t.Fatalf("preparing the importcfg failed: %s", err)
-	}
-	err = os.WriteFile(dstPath, []byte(importcfg), 0655)
-	if err != nil {
-		t.Fatalf("writing the importcfg failed: %s", err)
-	}*/
+	panic("not implemented, not needed by Hugo")
 }
diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go
index 97c92a6e949..2f72080c2b0 100644
--- a/tpl/internal/go_templates/testenv/testenv_test.go
+++ b/tpl/internal/go_templates/testenv/testenv_test.go
@@ -13,7 +13,8 @@ import (
 	"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
 )
 
-func _TestGoToolLocation(t *testing.T) {
+func TestGoToolLocation(t *testing.T) {
+	t.Skip("skipping test that requires go command")
 	testenv.MustHaveGoBuild(t)
 
 	var exeSuffix string
diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go
index 45edb9e9bb2..b44d61bb116 100644
--- a/tpl/internal/go_templates/texttemplate/exec_test.go
+++ b/tpl/internal/go_templates/texttemplate/exec_test.go
@@ -697,6 +697,7 @@ var execTests = []execTest{
 	{"bug18c", "{{eq . 'P'}}", "true", 'P', true},
 
 	{"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true},
+	{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
 }
 
 func zeroArgs() string {
diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go
index 4107a1faa35..fa511fbab35 100644
--- a/tpl/tplimpl/integration_test.go
+++ b/tpl/tplimpl/integration_test.go
@@ -2,6 +2,7 @@ package tplimpl_test
 
 import (
 	"path/filepath"
+	"strings"
 	"testing"
 
 	qt "github.com/frankban/quicktest"
@@ -160,3 +161,70 @@ title: "S3P1"
 	b.AssertFileContent("public/s2/p1/index.html", `S2P1`)
 	b.AssertFileContent("public/s3/p1/index.html", `S3P1`)
 }
+
+func TestGoTemplateBugs(t *testing.T) {
+
+	t.Run("Issue 11112", func(t *testing.T) {
+		t.Parallel()
+
+		files := `
+-- config.toml --
+-- layouts/index.html --
+{{ $m := dict "key" "value" }}
+{{ $k := "" }}
+{{ $v := "" }}
+{{ range $k, $v = $m }}
+{{ $k }} = {{ $v }}
+{{ end }}
+	`
+
+		b := hugolib.NewIntegrationTestBuilder(
+			hugolib.IntegrationTestConfig{
+				T:           t,
+				TxtarString: files,
+			},
+		)
+		b.Build()
+
+		b.AssertFileContent("public/index.html", `key = value`)
+	})
+
+}
+
+func TestSecurityAllowActionJSTmpl(t *testing.T) {
+
+	filesTemplate := `
+-- config.toml --
+SECURITYCONFIG
+-- layouts/index.html --
+<script>
+var a = §§{{.Title }}§§;
+</script>
+	`
+
+	files := strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", "")
+
+	b, err := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).BuildE()
+
+	b.Assert(err, qt.Not(qt.IsNil))
+	b.Assert(err.Error(), qt.Contains, "{{.Title}} appears in a JS template literal")
+
+	files = strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", `
+[security]
+[security.gotemplates]
+allowActionJSTmpl = true		
+`)
+
+	b = hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+}