src/html/template/context.go | 2 ++ src/html/template/error.go | 13 +++++++++++++ src/html/template/escape.go | 11 +++++++++++ src/html/template/escape_test.go | 66 ++++++++++++++++++++++++++++++----------------------- src/html/template/js.go | 2 ++ src/html/template/js_test.go | 2 +- src/html/template/jsctx_string.go | 9 +++++++++ src/html/template/state_string.go | 37 +++++++++++++++++++++++++++++++++++-- src/html/template/transition.go | 7 ++++++- diff --git a/src/html/template/context.go b/src/html/template/context.go index a97c8be56f958e4894e0dc1d16efb8a16f745e89..c28fb0c5ea8ef30ddbcb4ffec6d0bef34756ad76 100644 --- a/src/html/template/context.go +++ b/src/html/template/context.go @@ -120,6 +120,8 @@ // stateJSDqStr occurs inside a JavaScript double quoted string. 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/src/html/template/error.go b/src/html/template/error.go index 5c51f772cbde36f4b08e217d94def0f21a13e896..d7d6f5b3ab589ea1e44f235ba7e2f2716906a8a4 100644 --- a/src/html/template/error.go +++ b/src/html/template/error.go @@ -214,6 +214,19 @@ // continue to be allowed as the last command in a pipeline. However, if the // 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: + // + // 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/src/html/template/escape.go b/src/html/template/escape.go index 54fbcdca333549cedae379361a3e9ac84597bed7..3d4cc19b5da85c22859b1182d053b1a19adba2ef 100644 --- a/src/html/template/escape.go +++ b/src/html/template/escape.go @@ -8,6 +8,7 @@ import ( "bytes" "fmt" "html" + "internal/godebug" "io" "text/template" "text/template/parse" @@ -223,6 +224,16 @@ // A slash after a value starts a div operator. c.jsCtx = jsCtxDivOp case stateJSDqStr, stateJSSqStr: s = append(s, "_html_template_jsstrescaper") + case stateJSBqStr: + debugAllowActionJSTmpl := godebug.Get("jstmpllitinterp") + if debugAllowActionJSTmpl == "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: diff --git a/src/html/template/escape_test.go b/src/html/template/escape_test.go index 58f3f271b7a5c39cb5ce8bc6e9f5edae7444393d..972b00b921d35be8eeec5a462b6d7657826a0ea6 100644 --- a/src/html/template/escape_test.go +++ b/src/html/template/escape_test.go @@ -681,35 +681,31 @@ }, } 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(bytes.Buffer) - 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) + } + }) } } @@ -934,6 +930,10 @@ "", }, { "{{range .Items}}{{if .X}}{{break}}{{end}}{{end}}", + "", + }, + { + "`", "", }, // Error cases. @@ -1082,6 +1082,10 @@ `Hello, {{. | urlquery | html}}!`, // html is allowed since it is the last command in the pipeline, but urlquery is not. `predefined escaper "urlquery" disallowed in template`, }, + { + "", + `{{.}} appears in a JS template literal`, + }, } for _, test := range tests { buf := new(bytes.Buffer) @@ -1302,6 +1306,10 @@ }, { `= state(len(_state_index)-1) { diff --git a/src/html/template/transition.go b/src/html/template/transition.go index 06df679330db2c49d1bfc7d2369f2bc2a40f164a..92eb3519063c9ea0fd17eb8344939ab4871d8ba8 100644 --- a/src/html/template/transition.go +++ b/src/html/template/transition.go @@ -27,6 +27,7 @@ stateSrcset: tURL, stateJS: tJS, stateJSDqStr: tJSDelimited, stateJSSqStr: tJSDelimited, + stateJSBqStr: tJSDelimited, stateJSRegexp: tJSDelimited, stateJSBlockCmt: tBlockCmt, stateJSLineCmt: tLineCmt, @@ -262,7 +263,7 @@ } // 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 @@ case '"': 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 @@ specials := `\"` switch c.state { case stateJSSqStr: specials = `\'` + case stateJSBqStr: + specials = "`\\" case stateJSRegexp: specials = `\/[]` }