aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/next/61619.txt1
-rw-r--r--src/html/template/context.go24
-rw-r--r--src/html/template/error.go4
-rw-r--r--src/html/template/escape.go48
-rw-r--r--src/html/template/escape_test.go135
-rw-r--r--src/html/template/js.go30
-rw-r--r--src/html/template/state_string.go6
-rw-r--r--src/html/template/transition.go63
8 files changed, 253 insertions, 58 deletions
diff --git a/api/next/61619.txt b/api/next/61619.txt
new file mode 100644
index 0000000000..c63a3140e8
--- /dev/null
+++ b/api/next/61619.txt
@@ -0,0 +1 @@
+pkg html/template, const ErrJSTemplate //deprecated #61619
diff --git a/src/html/template/context.go b/src/html/template/context.go
index 16b5e65317..63d5c31b01 100644
--- a/src/html/template/context.go
+++ b/src/html/template/context.go
@@ -17,14 +17,16 @@ import (
// https://www.w3.org/TR/html5/syntax.html#the-end
// where the context element is null.
type context struct {
- state state
- delim delim
- urlPart urlPart
- jsCtx jsCtx
- attr attr
- element element
- n parse.Node // for range break/continue
- err *Error
+ state state
+ delim delim
+ urlPart urlPart
+ jsCtx jsCtx
+ jsTmplExprDepth int
+ jsBraceDepth int
+ attr attr
+ element element
+ n parse.Node // for range break/continue
+ err *Error
}
func (c context) String() string {
@@ -120,8 +122,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 */.
@@ -182,7 +184,7 @@ func isInScriptLiteral(s state) bool {
// 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/src/html/template/error.go b/src/html/template/error.go
index a763924d4a..805a788bfc 100644
--- a/src/html/template/error.go
+++ b/src/html/template/error.go
@@ -221,6 +221,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/src/html/template/escape.go b/src/html/template/escape.go
index 01f6303a44..1eace16e25 100644
--- a/src/html/template/escape.go
+++ b/src/html/template/escape.go
@@ -62,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
@@ -227,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 debugAllowActionJSTmpl.Value() == "1" {
- 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:
@@ -395,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/src/html/template/escape_test.go b/src/html/template/escape_test.go
index 8a4f62e92f..9e2f4fe922 100644
--- a/src/html/template/escape_test.go
+++ b/src/html/template/escape_test.go
@@ -30,14 +30,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 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 HTML
}{
F: false,
T: true,
@@ -52,6 +52,7 @@ func TestEscape(t *testing.T) {
U: nil,
Z: nil,
W: HTML(`&iexcl;<b class="foo">Hello</b>, <textarea>O'World</textarea>!`),
+ I: "${ asd `` }",
}
pdata := &data
@@ -718,6 +719,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 {
@@ -976,6 +992,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}}",
@@ -1122,10 +1163,26 @@ 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`,
- },
+ // {
+ // "<script>var tmpl = `asd {{.}}`;</script>",
+ // `{{.}} appears in a JS template literal`,
+ // },
+ // {
+ // "<script>var v = `${function(){return `{{.V}}+1`}()}`;</script>",
+ // `{{.V}} appears in a JS template literal`,
+ // },
+ // {
+ // "<script>var a = `asd ${function(){b = {1:2}; return`{{.}}`}}`</script>",
+ // `{{.}} appears in a JS template literal`,
+ // },
+ // {
+ // "<script>var tmpl = `${return `{{.}}`}`;</script>",
+ // `{{.}} appears in a JS template literal`,
+ // },
+ // {
+ // "<script>var tmpl = `${return {`{{.}}`}`;</script>",
+ // `{{.}} appears in a JS template literal`,
+ // },
}
for _, test := range tests {
buf := new(bytes.Buffer)
@@ -1349,7 +1406,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="'`,
@@ -1691,6 +1748,58 @@ 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},
+ },
}
for _, test := range tests {
diff --git a/src/html/template/js.go b/src/html/template/js.go
index 717de4300c..b159af8e4b 100644
--- a/src/html/template/js.go
+++ b/src/html/template/js.go
@@ -238,6 +238,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
@@ -324,6 +329,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{
diff --git a/src/html/template/state_string.go b/src/html/template/state_string.go
index be7a920511..eed1e8bcc0 100644
--- a/src/html/template/state_string.go
+++ b/src/html/template/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/src/html/template/transition.go b/src/html/template/transition.go
index 432c365d3c..4ea803e428 100644
--- a/src/html/template/transition.go
+++ b/src/html/template/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,67 @@ func tJS(c context, s []byte) (context, int) {
if i+1 < len(s) && s[i+1] == '!' {
c.state, i = stateJSLineCmt, i+1
}
+ case '{':
+ c.jsBraceDepth++
+ case '}':
+ if c.jsTmplExprDepth == 0 {
+ return c, i + 1
+ }
+ for j := 0; j <= i; j++ {
+ switch s[j] {
+ case '\\':
+ j++
+ case '{':
+ c.jsBraceDepth++
+ case '}':
+ c.jsBraceDepth--
+ }
+ }
+ if c.jsBraceDepth >= 0 {
+ return c, i + 1
+ }
+ c.jsTmplExprDepth--
+ c.jsBraceDepth = 0
+ 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.jsTmplExprDepth++
+ 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 +388,6 @@ func tJSDelimited(c context, s []byte) (context, int) {
switch c.state {
case stateJSSqStr:
specials = `\'`
- case stateJSBqStr:
- specials = "`\\"
case stateJSRegexp:
specials = `\/[]`
}