Skip to content

Commit ecc5ba4

Browse files
rolandshoemakergopherbot
authored andcommitted
html/template: disallow actions in JS template literals
ECMAScript 6 introduced template literals[0][1] which are delimited with backticks. These need to be escaped in a similar fashion to the delimiters for other string literals. Additionally template literals can contain special syntax for string interpolation. There is no clear way to allow safe insertion of actions within JS template literals, as handling (JS) string interpolation inside of these literals is rather complex. As such we've chosen to simply disallow template actions within these template literals. A new error code is added for this parsing failure case, errJsTmplLit, but it is unexported as it is not backwards compatible with other minor release versions to introduce an API change in a minor release. We will export this code in the next major release. The previous behavior (with the cavet that backticks are now escaped properly) can be re-enabled with GODEBUG=jstmpllitinterp=1. This change subsumes CL471455. Thanks to Sohom Datta, Manipal Institute of Technology, for reporting this issue. Fixes CVE-2023-24538 Fixes #59234 [0] https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-template-literals [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802457 Reviewed-by: Damien Neil <[email protected]> Run-TryBot: Damien Neil <[email protected]> Reviewed-by: Julie Qiu <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Change-Id: Ia221fefdb273bd0f066dffc2abcf2a616801d2f2 Reviewed-on: https://go-review.googlesource.com/c/go/+/482079 TryBot-Bypass: Michael Knyszek <[email protected]> Run-TryBot: Michael Knyszek <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]> Auto-Submit: Michael Knyszek <[email protected]>
1 parent 110e4fb commit ecc5ba4

File tree

12 files changed

+131
-33
lines changed

12 files changed

+131
-33
lines changed

src/html/template/context.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ const (
120120
stateJSDqStr
121121
// stateJSSqStr occurs inside a JavaScript single quoted string.
122122
stateJSSqStr
123+
// stateJSBqStr occurs inside a JavaScript back quoted string.
124+
stateJSBqStr
123125
// stateJSRegexp occurs inside a JavaScript regexp literal.
124126
stateJSRegexp
125127
// stateJSBlockCmt occurs inside a JavaScript /* block comment */.

src/html/template/error.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,19 @@ const (
214214
// pipeline occurs in an unquoted attribute value context, "html" is
215215
// disallowed. Avoid using "html" and "urlquery" entirely in new templates.
216216
ErrPredefinedEscaper
217+
218+
// errJSTmplLit: "... appears in a JS template literal"
219+
// Example:
220+
// <script>var tmpl = `{{.Interp}`</script>
221+
// Discussion:
222+
// Package html/template does not support actions inside of JS template
223+
// literals.
224+
//
225+
// TODO(rolandshoemaker): we cannot add this as an exported error in a minor
226+
// release, since it is backwards incompatible with the other minor
227+
// releases. As such we need to leave it unexported, and then we'll add it
228+
// in the next major release.
229+
errJSTmplLit
217230
)
218231

219232
func (e *Error) Error() string {

src/html/template/escape.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"fmt"
1010
"html"
11+
"internal/godebug"
1112
"io"
1213
"text/template"
1314
"text/template/parse"
@@ -160,6 +161,8 @@ func (e *escaper) escape(c context, n parse.Node) context {
160161
panic("escaping " + n.String() + " is unimplemented")
161162
}
162163

164+
var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
165+
163166
// escapeAction escapes an action template node.
164167
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
165168
if len(n.Pipe.Decl) != 0 {
@@ -223,6 +226,16 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
223226
c.jsCtx = jsCtxDivOp
224227
case stateJSDqStr, stateJSSqStr:
225228
s = append(s, "_html_template_jsstrescaper")
229+
case stateJSBqStr:
230+
if debugAllowActionJSTmpl.Value() == "1" {
231+
debugAllowActionJSTmpl.IncNonDefault()
232+
s = append(s, "_html_template_jsstrescaper")
233+
} else {
234+
return context{
235+
state: stateError,
236+
err: errorf(errJSTmplLit, n, n.Line, "%s appears in a JS template literal", n),
237+
}
238+
}
226239
case stateJSRegexp:
227240
s = append(s, "_html_template_jsregexpescaper")
228241
case stateCSS:

src/html/template/escape_test.go

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -681,35 +681,31 @@ func TestEscape(t *testing.T) {
681681
}
682682

683683
for _, test := range tests {
684-
tmpl := New(test.name)
685-
tmpl = Must(tmpl.Parse(test.input))
686-
// Check for bug 6459: Tree field was not set in Parse.
687-
if tmpl.Tree != tmpl.text.Tree {
688-
t.Errorf("%s: tree not set properly", test.name)
689-
continue
690-
}
691-
b := new(strings.Builder)
692-
if err := tmpl.Execute(b, data); err != nil {
693-
t.Errorf("%s: template execution failed: %s", test.name, err)
694-
continue
695-
}
696-
if w, g := test.output, b.String(); w != g {
697-
t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
698-
continue
699-
}
700-
b.Reset()
701-
if err := tmpl.Execute(b, pdata); err != nil {
702-
t.Errorf("%s: template execution failed for pointer: %s", test.name, err)
703-
continue
704-
}
705-
if w, g := test.output, b.String(); w != g {
706-
t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
707-
continue
708-
}
709-
if tmpl.Tree != tmpl.text.Tree {
710-
t.Errorf("%s: tree mismatch", test.name)
711-
continue
712-
}
684+
t.Run(test.name, func(t *testing.T) {
685+
tmpl := New(test.name)
686+
tmpl = Must(tmpl.Parse(test.input))
687+
// Check for bug 6459: Tree field was not set in Parse.
688+
if tmpl.Tree != tmpl.text.Tree {
689+
t.Fatalf("%s: tree not set properly", test.name)
690+
}
691+
b := new(strings.Builder)
692+
if err := tmpl.Execute(b, data); err != nil {
693+
t.Fatalf("%s: template execution failed: %s", test.name, err)
694+
}
695+
if w, g := test.output, b.String(); w != g {
696+
t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
697+
}
698+
b.Reset()
699+
if err := tmpl.Execute(b, pdata); err != nil {
700+
t.Fatalf("%s: template execution failed for pointer: %s", test.name, err)
701+
}
702+
if w, g := test.output, b.String(); w != g {
703+
t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
704+
}
705+
if tmpl.Tree != tmpl.text.Tree {
706+
t.Fatalf("%s: tree mismatch", test.name)
707+
}
708+
})
713709
}
714710
}
715711

@@ -936,6 +932,10 @@ func TestErrors(t *testing.T) {
936932
"{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
937933
"",
938934
},
935+
{
936+
"<script>var a = `${a+b}`</script>`",
937+
"",
938+
},
939939
// Error cases.
940940
{
941941
"{{if .Cond}}<a{{end}}",
@@ -1082,6 +1082,10 @@ func TestErrors(t *testing.T) {
10821082
// html is allowed since it is the last command in the pipeline, but urlquery is not.
10831083
`predefined escaper "urlquery" disallowed in template`,
10841084
},
1085+
{
1086+
"<script>var tmpl = `asd {{.}}`;</script>",
1087+
`{{.}} appears in a JS template literal`,
1088+
},
10851089
}
10861090
for _, test := range tests {
10871091
buf := new(bytes.Buffer)
@@ -1303,6 +1307,10 @@ func TestEscapeText(t *testing.T) {
13031307
`<a onclick="'foo&quot;`,
13041308
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
13051309
},
1310+
{
1311+
"<a onclick=\"`foo",
1312+
context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript},
1313+
},
13061314
{
13071315
`<A ONCLICK="'`,
13081316
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},

src/html/template/js.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ var jsStrReplacementTable = []string{
308308
// Encode HTML specials as hex so the output can be embedded
309309
// in HTML attributes without further encoding.
310310
'"': `\u0022`,
311+
'`': `\u0060`,
311312
'&': `\u0026`,
312313
'\'': `\u0027`,
313314
'+': `\u002b`,
@@ -331,6 +332,7 @@ var jsStrNormReplacementTable = []string{
331332
'"': `\u0022`,
332333
'&': `\u0026`,
333334
'\'': `\u0027`,
335+
'`': `\u0060`,
334336
'+': `\u002b`,
335337
'/': `\/`,
336338
'<': `\u003c`,

src/html/template/js_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
291291
`0123456789:;\u003c=\u003e?` +
292292
`@ABCDEFGHIJKLMNO` +
293293
`PQRSTUVWXYZ[\\]^_` +
294-
"`abcdefghijklmno" +
294+
"\\u0060abcdefghijklmno" +
295295
"pqrstuvwxyz{|}~\u007f" +
296296
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
297297
},

src/html/template/jsctx_string.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/html/template/state_string.go

Lines changed: 35 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/html/template/transition.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){
2727
stateJS: tJS,
2828
stateJSDqStr: tJSDelimited,
2929
stateJSSqStr: tJSDelimited,
30+
stateJSBqStr: tJSDelimited,
3031
stateJSRegexp: tJSDelimited,
3132
stateJSBlockCmt: tBlockCmt,
3233
stateJSLineCmt: tLineCmt,
@@ -262,7 +263,7 @@ func tURL(c context, s []byte) (context, int) {
262263

263264
// tJS is the context transition function for the JS state.
264265
func tJS(c context, s []byte) (context, int) {
265-
i := bytes.IndexAny(s, `"'/`)
266+
i := bytes.IndexAny(s, "\"`'/")
266267
if i == -1 {
267268
// Entire input is non string, comment, regexp tokens.
268269
c.jsCtx = nextJSCtx(s, c.jsCtx)
@@ -274,6 +275,8 @@ func tJS(c context, s []byte) (context, int) {
274275
c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
275276
case '\'':
276277
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
278+
case '`':
279+
c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp
277280
case '/':
278281
switch {
279282
case i+1 < len(s) && s[i+1] == '/':
@@ -303,6 +306,8 @@ func tJSDelimited(c context, s []byte) (context, int) {
303306
switch c.state {
304307
case stateJSSqStr:
305308
specials = `\'`
309+
case stateJSBqStr:
310+
specials = "`\\"
306311
case stateJSRegexp:
307312
specials = `\/[]`
308313
}

src/runtime/metrics.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ func initMetrics() {
290290
"/godebug/non-default-behavior/http2client:events": {compute: compute0},
291291
"/godebug/non-default-behavior/http2server:events": {compute: compute0},
292292
"/godebug/non-default-behavior/installgoroot:events": {compute: compute0},
293+
"/godebug/non-default-behavior/jstmpllitinterp:events": {compute: compute0},
293294
"/godebug/non-default-behavior/multipartfiles:events": {compute: compute0},
294295
"/godebug/non-default-behavior/multipartmaxheaders:events": {compute: compute0},
295296
"/godebug/non-default-behavior/multipartmaxparts:events": {compute: compute0},

src/runtime/metrics/description.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ var allDesc = []Description{
305305
Kind: KindUint64,
306306
Cumulative: true,
307307
},
308+
{
309+
Name: "/godebug/non-default-behavior/jstmpllitinterp:events",
310+
Description: "The number of non-default behaviors executed by the html/template" +
311+
"package due to a non-default GODEBUG=jstmpllitinterp=... setting.",
312+
Kind: KindUint64,
313+
Cumulative: true,
314+
},
308315
{
309316
Name: "/godebug/non-default-behavior/multipartfiles:events",
310317
Description: "The number of non-default behaviors executed by the mime/multipart package " +

src/runtime/metrics/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ Below is the full list of supported metrics, ordered lexicographically.
219219
The number of non-default behaviors executed by the go/build
220220
package due to a non-default GODEBUG=installgoroot=... setting.
221221
222+
/godebug/non-default-behavior/jstmpllitinterp:events
223+
The number of non-default behaviors executed by
224+
the html/templatepackage due to a non-default
225+
GODEBUG=jstmpllitinterp=... setting.
226+
222227
/godebug/non-default-behavior/multipartfiles:events
223228
The number of non-default behaviors executed by
224229
the mime/multipart package due to a non-default

0 commit comments

Comments
 (0)