Skip to content

Commit 056b0ed

Browse files
rolandshoemakercagedmantis
authored andcommitted
[release-branch.go1.22] html/template: escape additional tokens in MarshalJSON errors
Escape "</script" and "<!--" in errors returned from MarshalJSON errors when attempting to marshal types in script blocks. This prevents any user controlled content from prematurely terminating the script block. Updates #65697 Fixes #65969 Change-Id: Icf0e26c54ea7d9c1deed0bff11b6506c99ddef1b Reviewed-on: https://go-review.googlesource.com/c/go/+/564196 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Damien Neil <[email protected]> (cherry picked from commit ccbc725) Reviewed-on: https://go-review.googlesource.com/c/go/+/567535 Reviewed-by: Carlos Amedee <[email protected]>
1 parent f73eba7 commit 056b0ed

File tree

2 files changed

+74
-44
lines changed

2 files changed

+74
-44
lines changed

src/html/template/js.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,31 @@ func jsValEscaper(args ...any) string {
171171
// cyclic data. This may be an unacceptable DoS risk.
172172
b, err := json.Marshal(a)
173173
if err != nil {
174-
// Put a space before comment so that if it is flush against
174+
// While the standard JSON marshaller does not include user controlled
175+
// information in the error message, if a type has a MarshalJSON method,
176+
// the content of the error message is not guaranteed. Since we insert
177+
// the error into the template, as part of a comment, we attempt to
178+
// prevent the error from either terminating the comment, or the script
179+
// block itself.
180+
//
181+
// In particular we:
182+
// * replace "*/" comment end tokens with "* /", which does not
183+
// terminate the comment
184+
// * replace "</script" with "\x3C/script", and "<!--" with
185+
// "\x3C!--", which prevents confusing script block termination
186+
// semantics
187+
//
188+
// We also put a space before the comment so that if it is flush against
175189
// a division operator it is not turned into a line comment:
176190
// x/{{y}}
177191
// turning into
178192
// x//* error marshaling y:
179193
// second line of error message */null
180-
return fmt.Sprintf(" /* %s */null ", strings.ReplaceAll(err.Error(), "*/", "* /"))
194+
errStr := err.Error()
195+
errStr = strings.ReplaceAll(errStr, "*/", "* /")
196+
errStr = strings.ReplaceAll(errStr, "</script", `\x3C/script`)
197+
errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
198+
return fmt.Sprintf(" /* %s */null ", errStr)
181199
}
182200

183201
// TODO: maybe post-process output to prevent it from containing

src/html/template/js_test.go

+54-42
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package template
66

77
import (
8+
"errors"
89
"math"
910
"strings"
1011
"testing"
@@ -103,61 +104,72 @@ func TestNextJsCtx(t *testing.T) {
103104
}
104105
}
105106

107+
type jsonErrType struct{}
108+
109+
func (e *jsonErrType) MarshalJSON() ([]byte, error) {
110+
return nil, errors.New("beep */ boop </script blip <!--")
111+
}
112+
106113
func TestJSValEscaper(t *testing.T) {
107114
tests := []struct {
108-
x any
109-
js string
115+
x any
116+
js string
117+
skipNest bool
110118
}{
111-
{int(42), " 42 "},
112-
{uint(42), " 42 "},
113-
{int16(42), " 42 "},
114-
{uint16(42), " 42 "},
115-
{int32(-42), " -42 "},
116-
{uint32(42), " 42 "},
117-
{int16(-42), " -42 "},
118-
{uint16(42), " 42 "},
119-
{int64(-42), " -42 "},
120-
{uint64(42), " 42 "},
121-
{uint64(1) << 53, " 9007199254740992 "},
119+
{int(42), " 42 ", false},
120+
{uint(42), " 42 ", false},
121+
{int16(42), " 42 ", false},
122+
{uint16(42), " 42 ", false},
123+
{int32(-42), " -42 ", false},
124+
{uint32(42), " 42 ", false},
125+
{int16(-42), " -42 ", false},
126+
{uint16(42), " 42 ", false},
127+
{int64(-42), " -42 ", false},
128+
{uint64(42), " 42 ", false},
129+
{uint64(1) << 53, " 9007199254740992 ", false},
122130
// ulp(1 << 53) > 1 so this loses precision in JS
123131
// but it is still a representable integer literal.
124-
{uint64(1)<<53 + 1, " 9007199254740993 "},
125-
{float32(1.0), " 1 "},
126-
{float32(-1.0), " -1 "},
127-
{float32(0.5), " 0.5 "},
128-
{float32(-0.5), " -0.5 "},
129-
{float32(1.0) / float32(256), " 0.00390625 "},
130-
{float32(0), " 0 "},
131-
{math.Copysign(0, -1), " -0 "},
132-
{float64(1.0), " 1 "},
133-
{float64(-1.0), " -1 "},
134-
{float64(0.5), " 0.5 "},
135-
{float64(-0.5), " -0.5 "},
136-
{float64(0), " 0 "},
137-
{math.Copysign(0, -1), " -0 "},
138-
{"", `""`},
139-
{"foo", `"foo"`},
132+
{uint64(1)<<53 + 1, " 9007199254740993 ", false},
133+
{float32(1.0), " 1 ", false},
134+
{float32(-1.0), " -1 ", false},
135+
{float32(0.5), " 0.5 ", false},
136+
{float32(-0.5), " -0.5 ", false},
137+
{float32(1.0) / float32(256), " 0.00390625 ", false},
138+
{float32(0), " 0 ", false},
139+
{math.Copysign(0, -1), " -0 ", false},
140+
{float64(1.0), " 1 ", false},
141+
{float64(-1.0), " -1 ", false},
142+
{float64(0.5), " 0.5 ", false},
143+
{float64(-0.5), " -0.5 ", false},
144+
{float64(0), " 0 ", false},
145+
{math.Copysign(0, -1), " -0 ", false},
146+
{"", `""`, false},
147+
{"foo", `"foo"`, false},
140148
// Newlines.
141-
{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
149+
{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`, false},
142150
// "\v" == "v" on IE 6 so use "\u000b" instead.
143-
{"\t\x0b", `"\t\u000b"`},
144-
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
145-
{[]any{}, "[]"},
146-
{[]any{42, "foo", nil}, `[42,"foo",null]`},
147-
{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`},
148-
{"<!--", `"\u003c!--"`},
149-
{"-->", `"--\u003e"`},
150-
{"<![CDATA[", `"\u003c![CDATA["`},
151-
{"]]>", `"]]\u003e"`},
152-
{"</script", `"\u003c/script"`},
153-
{"\U0001D11E", "\"\U0001D11E\""}, // or "\uD834\uDD1E"
154-
{nil, " null "},
151+
{"\t\x0b", `"\t\u000b"`, false},
152+
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`, false},
153+
{[]any{}, "[]", false},
154+
{[]any{42, "foo", nil}, `[42,"foo",null]`, false},
155+
{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`, false},
156+
{"<!--", `"\u003c!--"`, false},
157+
{"-->", `"--\u003e"`, false},
158+
{"<![CDATA[", `"\u003c![CDATA["`, false},
159+
{"]]>", `"]]\u003e"`, false},
160+
{"</script", `"\u003c/script"`, false},
161+
{"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
162+
{nil, " null ", false},
163+
{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: beep * / boop \\x3C/script blip \\x3C!-- */null ", true},
155164
}
156165

157166
for _, test := range tests {
158167
if js := jsValEscaper(test.x); js != test.js {
159168
t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js)
160169
}
170+
if test.skipNest {
171+
continue
172+
}
161173
// Make sure that escaping corner cases are not broken
162174
// by nesting.
163175
a := []any{test.x}

0 commit comments

Comments
 (0)