diff options
-rw-r--r-- | src/html/template/context.go | 2 | ||||
-rw-r--r-- | src/html/template/error.go | 13 | ||||
-rw-r--r-- | src/html/template/escape.go | 11 | ||||
-rw-r--r-- | src/html/template/escape_test.go | 66 | ||||
-rw-r--r-- | src/html/template/js.go | 2 | ||||
-rw-r--r-- | src/html/template/js_test.go | 2 | ||||
-rw-r--r-- | src/html/template/jsctx_string.go | 9 | ||||
-rw-r--r-- | src/html/template/state_string.go | 37 | ||||
-rw-r--r-- | src/html/template/transition.go | 7 |
9 files changed, 116 insertions, 33 deletions
diff --git a/src/html/template/context.go b/src/html/template/context.go index a97c8be56f..c28fb0c5ea 100644 --- a/src/html/template/context.go +++ b/src/html/template/context.go @@ -120,6 +120,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/src/html/template/error.go b/src/html/template/error.go index 5c51f772cb..d7d6f5b3ab 100644 --- a/src/html/template/error.go +++ b/src/html/template/error.go @@ -214,6 +214,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/src/html/template/escape.go b/src/html/template/escape.go index 54fbcdca33..3d4cc19b5d 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 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { 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 58f3f271b7..972b00b921 100644 --- a/src/html/template/escape_test.go +++ b/src/html/template/escape_test.go @@ -681,35 +681,31 @@ func TestEscape(t *testing.T) { } 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) + } + }) } } @@ -936,6 +932,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}}", @@ -1082,6 +1082,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) @@ -1304,6 +1308,10 @@ func TestEscapeText(t *testing.T) { 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/src/html/template/js.go b/src/html/template/js.go index 50523d00f1..fe7054efe5 100644 --- a/src/html/template/js.go +++ b/src/html/template/js.go @@ -308,6 +308,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`, @@ -331,6 +332,7 @@ var jsStrNormReplacementTable = []string{ '"': `\u0022`, '&': `\u0026`, '\'': `\u0027`, + '`': `\u0060`, '+': `\u002b`, '/': `\/`, '<': `\u003c`, diff --git a/src/html/template/js_test.go b/src/html/template/js_test.go index 56579d8d30..e07c695f7a 100644 --- a/src/html/template/js_test.go +++ b/src/html/template/js_test.go @@ -292,7 +292,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { `0123456789:;\u003c=\u003e?` + `@ABCDEFGHIJKLMNO` + `PQRSTUVWXYZ[\\]^_` + - "`abcdefghijklmno" + + "\\u0060abcdefghijklmno" + "pqrstuvwxyz{|}~\u007f" + "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", }, diff --git a/src/html/template/jsctx_string.go b/src/html/template/jsctx_string.go index dd1d87ee45..23948934c9 100644 --- a/src/html/template/jsctx_string.go +++ b/src/html/template/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/src/html/template/state_string.go b/src/html/template/state_string.go index 05104be89c..6fb1a6eeb0 100644 --- a/src/html/template/state_string.go +++ b/src/html/template/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/src/html/template/transition.go b/src/html/template/transition.go index 06df679330..92eb351906 100644 --- a/src/html/template/transition.go +++ b/src/html/template/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 = `\/[]` } |