Skip to content

Commit a1c1a76

Browse files
dspeziarobpike
authored andcommitted
html/template: fix string iteration in replacement operations
In css, js, and html, the replacement operations are implemented by iterating on strings (rune by rune). The for/range statement is used. The length of the rune is required and added to the index to properly slice the string. This is potentially wrong because there is a discrepancy between the result of utf8.RuneLen and the increment of the index (set by the for/range statement). For invalid strings, utf8.RuneLen('\ufffd') == 3, while the index is incremented only by 1 byte. htmlReplacer triggers a panic at slicing time for some invalid strings. Use a more robust iteration mechanism based on utf8.DecodeRuneInString, and make sure the same pattern is used for all similar functions in this package. Fixes #10799 Change-Id: Ibad3857b2819435d9fa564f06fc2ca8774102841 Reviewed-on: https://go-review.googlesource.com/10105 Reviewed-by: Rob Pike <[email protected]>
1 parent d6bbcea commit a1c1a76

File tree

4 files changed

+51
-55
lines changed

4 files changed

+51
-55
lines changed

src/html/template/css.go

+32-44
Original file line numberDiff line numberDiff line change
@@ -157,56 +157,20 @@ func isCSSSpace(b byte) bool {
157157
func cssEscaper(args ...interface{}) string {
158158
s, _ := stringify(args...)
159159
var b bytes.Buffer
160-
written := 0
161-
for i, r := range s {
160+
r, w, written := rune(0), 0, 0
161+
for i := 0; i < len(s); i += w {
162+
// See comment in htmlEscaper.
163+
r, w = utf8.DecodeRuneInString(s[i:])
162164
var repl string
163-
switch r {
164-
case 0:
165-
repl = `\0`
166-
case '\t':
167-
repl = `\9`
168-
case '\n':
169-
repl = `\a`
170-
case '\f':
171-
repl = `\c`
172-
case '\r':
173-
repl = `\d`
174-
// Encode HTML specials as hex so the output can be embedded
175-
// in HTML attributes without further encoding.
176-
case '"':
177-
repl = `\22`
178-
case '&':
179-
repl = `\26`
180-
case '\'':
181-
repl = `\27`
182-
case '(':
183-
repl = `\28`
184-
case ')':
185-
repl = `\29`
186-
case '+':
187-
repl = `\2b`
188-
case '/':
189-
repl = `\2f`
190-
case ':':
191-
repl = `\3a`
192-
case ';':
193-
repl = `\3b`
194-
case '<':
195-
repl = `\3c`
196-
case '>':
197-
repl = `\3e`
198-
case '\\':
199-
repl = `\\`
200-
case '{':
201-
repl = `\7b`
202-
case '}':
203-
repl = `\7d`
165+
switch {
166+
case int(r) < len(cssReplacementTable) && cssReplacementTable[r] != "":
167+
repl = cssReplacementTable[r]
204168
default:
205169
continue
206170
}
207171
b.WriteString(s[written:i])
208172
b.WriteString(repl)
209-
written = i + utf8.RuneLen(r)
173+
written = i + w
210174
if repl != `\\` && (written == len(s) || isHex(s[written]) || isCSSSpace(s[written])) {
211175
b.WriteByte(' ')
212176
}
@@ -218,6 +182,30 @@ func cssEscaper(args ...interface{}) string {
218182
return b.String()
219183
}
220184

185+
var cssReplacementTable = []string{
186+
0: `\0`,
187+
'\t': `\9`,
188+
'\n': `\a`,
189+
'\f': `\c`,
190+
'\r': `\d`,
191+
// Encode HTML specials as hex so the output can be embedded
192+
// in HTML attributes without further encoding.
193+
'"': `\22`,
194+
'&': `\26`,
195+
'\'': `\27`,
196+
'(': `\28`,
197+
')': `\29`,
198+
'+': `\2b`,
199+
'/': `\2f`,
200+
':': `\3a`,
201+
';': `\3b`,
202+
'<': `\3c`,
203+
'>': `\3e`,
204+
'\\': `\\`,
205+
'{': `\7b`,
206+
'}': `\7d`,
207+
}
208+
221209
var expressionBytes = []byte("expression")
222210
var mozBindingBytes = []byte("mozbinding")
223211

src/html/template/html.go

+8-5
Original file line numberDiff line numberDiff line change
@@ -138,21 +138,24 @@ var htmlNospaceNormReplacementTable = []string{
138138
// and when badRunes is true, certain bad runes are allowed through unescaped.
139139
func htmlReplacer(s string, replacementTable []string, badRunes bool) string {
140140
written, b := 0, new(bytes.Buffer)
141-
for i, r := range s {
141+
r, w := rune(0), 0
142+
for i := 0; i < len(s); i += w {
143+
// Cannot use 'for range s' because we need to preserve the width
144+
// of the runes in the input. If we see a decoding error, the input
145+
// width will not be utf8.Runelen(r) and we will overrun the buffer.
146+
r, w = utf8.DecodeRuneInString(s[i:])
142147
if int(r) < len(replacementTable) {
143148
if repl := replacementTable[r]; len(repl) != 0 {
144149
b.WriteString(s[written:i])
145150
b.WriteString(repl)
146-
// Valid as long as replacementTable doesn't
147-
// include anything above 0x7f.
148-
written = i + utf8.RuneLen(r)
151+
written = i + w
149152
}
150153
} else if badRunes {
151154
// No-op.
152155
// IE does not allow these ranges in unquoted attrs.
153156
} else if 0xfdd0 <= r && r <= 0xfdef || 0xfff0 <= r && r <= 0xffff {
154157
fmt.Fprintf(b, "%s&#x%x;", s[written:i], r)
155-
written = i + utf8.RuneLen(r)
158+
written = i + w
156159
}
157160
}
158161
if written == 0 {

src/html/template/html_test.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ func TestHTMLNospaceEscaper(t *testing.T) {
1919
`PQRSTUVWXYZ[\]^_` +
2020
"`abcdefghijklmno" +
2121
"pqrstuvwxyz{|}~\x7f" +
22-
"\u00A0\u0100\u2028\u2029\ufeff\ufdec\U0001D11E")
22+
"\u00A0\u0100\u2028\u2029\ufeff\ufdec\U0001D11E" +
23+
"erroneous\x960") // keep at the end
2324

2425
want := ("&#xfffd;\x01\x02\x03\x04\x05\x06\x07" +
2526
"\x08&#9;&#10;&#11;&#12;&#13;\x0E\x0F" +
@@ -31,14 +32,16 @@ func TestHTMLNospaceEscaper(t *testing.T) {
3132
`PQRSTUVWXYZ[\]^_` +
3233
`&#96;abcdefghijklmno` +
3334
`pqrstuvwxyz{|}~` + "\u007f" +
34-
"\u00A0\u0100\u2028\u2029\ufeff&#xfdec;\U0001D11E")
35+
"\u00A0\u0100\u2028\u2029\ufeff&#xfdec;\U0001D11E" +
36+
"erroneous&#xfffd;0") // keep at the end
3537

3638
got := htmlNospaceEscaper(input)
3739
if got != want {
3840
t.Errorf("encode: want\n\t%q\nbut got\n\t%q", want, got)
3941
}
4042

41-
got, want = html.UnescapeString(got), strings.Replace(input, "\x00", "\ufffd", 1)
43+
r := strings.NewReplacer("\x00", "\ufffd", "\x96", "\ufffd")
44+
got, want = html.UnescapeString(got), r.Replace(input)
4245
if want != got {
4346
t.Errorf("decode: want\n\t%q\nbut got\n\t%q", want, got)
4447
}

src/html/template/js.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,10 @@ func jsRegexpEscaper(args ...interface{}) string {
246246
// `\u2029`.
247247
func replace(s string, replacementTable []string) string {
248248
var b bytes.Buffer
249-
written := 0
250-
for i, r := range s {
249+
r, w, written := rune(0), 0, 0
250+
for i := 0; i < len(s); i += w {
251+
// See comment in htmlEscaper.
252+
r, w = utf8.DecodeRuneInString(s[i:])
251253
var repl string
252254
switch {
253255
case int(r) < len(replacementTable) && replacementTable[r] != "":
@@ -261,7 +263,7 @@ func replace(s string, replacementTable []string) string {
261263
}
262264
b.WriteString(s[written:i])
263265
b.WriteString(repl)
264-
written = i + utf8.RuneLen(r)
266+
written = i + w
265267
}
266268
if written == 0 {
267269
return s

0 commit comments

Comments
 (0)