From c9c885f92f540878d85c02b510c62a3ebf87baf6 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Wed, 5 Jul 2023 11:56:03 -0700 Subject: [PATCH] html/template: support parsing complex JS template literals This change undoes the restrictions added in CL 482079, which added a blanket ban on using actions within JS template literal strings, and adds logic to support actions while properly applies contextual escaping based on the correct context within the literal. Since template literals can contain both normal strings, and nested JS contexts, logic is required to properly track those context switches during parsing. ErrJsTmplLit is deprecated, and the GODEBUG flag jstmpllitinterp no longer does anything. Fixes #61619 Change-Id: I0338cc6f663723267b8f7aaacc55aa28f60906f2 Reviewed-on: https://go-review.googlesource.com/c/go/+/507995 LUCI-TryBot-Result: Go LUCI Auto-Submit: Roland Shoemaker Reviewed-by: Damien Neil --- api/next/61619.txt | 1 + src/html/template/context.go | 24 +++--- src/html/template/error.go | 4 + src/html/template/escape.go | 48 +++++------ src/html/template/escape_test.go | 135 +++++++++++++++++++++++++++--- src/html/template/js.go | 30 +++++++ src/html/template/state_string.go | 6 +- src/html/template/transition.go | 63 ++++++++++++-- 8 files changed, 253 insertions(+), 58 deletions(-) create mode 100644 api/next/61619.txt 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(`¡Hello, !`), + I: "${ asd `` }", } pdata := &data @@ -718,6 +719,21 @@ func TestEscape(t *testing.T) { "

", "

", }, + { + "JS template lit special characters", + "", + "", + }, + { + "JS template lit special characters, nested lit", + "", + "", + }, + { + "JS template lit, nested JS", + "", + "", + }, } for _, test := range tests { @@ -976,6 +992,31 @@ func TestErrors(t *testing.T) { "`", "", }, + { + "", + ``, + }, + { + "", + ``, + }, + { + "", + ``, + }, + { + "", + ``, + }, + { + "", + ``, + }, + { + "", + ``, + }, + // Error cases. { "{{if .Cond}}var tmpl = `asd {{.}}`;", - `{{.}} appears in a JS template literal`, - }, + // { + // "", + // `{{.}} appears in a JS template literal`, + // }, + // { + // "", + // `{{.V}} appears in a JS template literal`, + // }, + // { + // "", + // `{{.}} appears in a JS template literal`, + // }, + // { + // "", + // `{{.}} appears in a JS template literal`, + // }, + // { + // "", + // `{{.}} appears in a JS template literal`, + // }, } for _, test := range tests { buf := new(bytes.Buffer) @@ -1349,7 +1406,7 @@ func TestEscapeText(t *testing.T) { }, { "`, context{}, }, + { + "