1// Copyright 2011 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package template 6 7import ( 8 "errors" 9 "math" 10 "strings" 11 "testing" 12) 13 14func TestNextJsCtx(t *testing.T) { 15 tests := []struct { 16 jsCtx jsCtx 17 s string 18 }{ 19 // Statement terminators precede regexps. 20 {jsCtxRegexp, ";"}, 21 // This is not airtight. 22 // ({ valueOf: function () { return 1 } } / 2) 23 // is valid JavaScript but in practice, devs do not do this. 24 // A block followed by a statement starting with a RegExp is 25 // much more common: 26 // while (x) {...} /foo/.test(x) || panic() 27 {jsCtxRegexp, "}"}, 28 // But member, call, grouping, and array expression terminators 29 // precede div ops. 30 {jsCtxDivOp, ")"}, 31 {jsCtxDivOp, "]"}, 32 // At the start of a primary expression, array, or expression 33 // statement, expect a regexp. 34 {jsCtxRegexp, "("}, 35 {jsCtxRegexp, "["}, 36 {jsCtxRegexp, "{"}, 37 // Assignment operators precede regexps as do all exclusively 38 // prefix and binary operators. 39 {jsCtxRegexp, "="}, 40 {jsCtxRegexp, "+="}, 41 {jsCtxRegexp, "*="}, 42 {jsCtxRegexp, "*"}, 43 {jsCtxRegexp, "!"}, 44 // Whether the + or - is infix or prefix, it cannot precede a 45 // div op. 46 {jsCtxRegexp, "+"}, 47 {jsCtxRegexp, "-"}, 48 // An incr/decr op precedes a div operator. 49 // This is not airtight. In (g = ++/h/i) a regexp follows a 50 // pre-increment operator, but in practice devs do not try to 51 // increment or decrement regular expressions. 52 // (g++/h/i) where ++ is a postfix operator on g is much more 53 // common. 54 {jsCtxDivOp, "--"}, 55 {jsCtxDivOp, "++"}, 56 {jsCtxDivOp, "x--"}, 57 // When we have many dashes or pluses, then they are grouped 58 // left to right. 59 {jsCtxRegexp, "x---"}, // A postfix -- then a -. 60 // return followed by a slash returns the regexp literal or the 61 // slash starts a regexp literal in an expression statement that 62 // is dead code. 63 {jsCtxRegexp, "return"}, 64 {jsCtxRegexp, "return "}, 65 {jsCtxRegexp, "return\t"}, 66 {jsCtxRegexp, "return\n"}, 67 {jsCtxRegexp, "return\u2028"}, 68 // Identifiers can be divided and cannot validly be preceded by 69 // a regular expressions. Semicolon insertion cannot happen 70 // between an identifier and a regular expression on a new line 71 // because the one token lookahead for semicolon insertion has 72 // to conclude that it could be a div binary op and treat it as 73 // such. 74 {jsCtxDivOp, "x"}, 75 {jsCtxDivOp, "x "}, 76 {jsCtxDivOp, "x\t"}, 77 {jsCtxDivOp, "x\n"}, 78 {jsCtxDivOp, "x\u2028"}, 79 {jsCtxDivOp, "preturn"}, 80 // Numbers precede div ops. 81 {jsCtxDivOp, "0"}, 82 // Dots that are part of a number are div preceders. 83 {jsCtxDivOp, "0."}, 84 // Some JS interpreters treat NBSP as a normal space, so 85 // we must too in order to properly escape things. 86 {jsCtxRegexp, "=\u00A0"}, 87 } 88 89 for _, test := range tests { 90 if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx { 91 t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx) 92 } 93 if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx { 94 t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx) 95 } 96 } 97 98 if nextJSCtx([]byte(" "), jsCtxRegexp) != jsCtxRegexp { 99 t.Error("Blank tokens") 100 } 101 102 if nextJSCtx([]byte(" "), jsCtxDivOp) != jsCtxDivOp { 103 t.Error("Blank tokens") 104 } 105} 106 107type jsonErrType struct{} 108 109func (e *jsonErrType) MarshalJSON() ([]byte, error) { 110 return nil, errors.New("beep */ boop </script blip <!--") 111} 112 113func TestJSValEscaper(t *testing.T) { 114 tests := []struct { 115 x any 116 js string 117 skipNest bool 118 }{ 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}, 130 // ulp(1 << 53) > 1 so this loses precision in JS 131 // but it is still a representable integer literal. 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}, 148 // Newlines. 149 {"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`, false}, 150 // "\v" == "v" on IE 6 so use "\u000b" instead. 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}, 164 } 165 166 for _, test := range tests { 167 if js := jsValEscaper(test.x); js != test.js { 168 t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js) 169 } 170 if test.skipNest { 171 continue 172 } 173 // Make sure that escaping corner cases are not broken 174 // by nesting. 175 a := []any{test.x} 176 want := "[" + strings.TrimSpace(test.js) + "]" 177 if js := jsValEscaper(a); js != want { 178 t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js) 179 } 180 } 181} 182 183func TestJSStrEscaper(t *testing.T) { 184 tests := []struct { 185 x any 186 esc string 187 }{ 188 {"", ``}, 189 {"foo", `foo`}, 190 {"\u0000", `\u0000`}, 191 {"\t", `\t`}, 192 {"\n", `\n`}, 193 {"\r", `\r`}, 194 {"\u2028", `\u2028`}, 195 {"\u2029", `\u2029`}, 196 {"\\", `\\`}, 197 {"\\n", `\\n`}, 198 {"foo\r\nbar", `foo\r\nbar`}, 199 // Preserve attribute boundaries. 200 {`"`, `\u0022`}, 201 {`'`, `\u0027`}, 202 // Allow embedding in HTML without further escaping. 203 {`&`, `\u0026amp;`}, 204 // Prevent breaking out of text node and element boundaries. 205 {"</script>", `\u003c\/script\u003e`}, 206 {"<![CDATA[", `\u003c![CDATA[`}, 207 {"]]>", `]]\u003e`}, 208 // https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span 209 // "The text in style, script, title, and textarea elements 210 // must not have an escaping text span start that is not 211 // followed by an escaping text span end." 212 // Furthermore, spoofing an escaping text span end could lead 213 // to different interpretation of a </script> sequence otherwise 214 // masked by the escaping text span, and spoofing a start could 215 // allow regular text content to be interpreted as script 216 // allowing script execution via a combination of a JS string 217 // injection followed by an HTML text injection. 218 {"<!--", `\u003c!--`}, 219 {"-->", `--\u003e`}, 220 // From https://code.google.com/p/doctype/wiki/ArticleUtf7 221 {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-", 222 `\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`, 223 }, 224 // Invalid UTF-8 sequence 225 {"foo\xA0bar", "foo\xA0bar"}, 226 // Invalid unicode scalar value. 227 {"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"}, 228 } 229 230 for _, test := range tests { 231 esc := jsStrEscaper(test.x) 232 if esc != test.esc { 233 t.Errorf("%q: want %q got %q", test.x, test.esc, esc) 234 } 235 } 236} 237 238func TestJSRegexpEscaper(t *testing.T) { 239 tests := []struct { 240 x any 241 esc string 242 }{ 243 {"", `(?:)`}, 244 {"foo", `foo`}, 245 {"\u0000", `\u0000`}, 246 {"\t", `\t`}, 247 {"\n", `\n`}, 248 {"\r", `\r`}, 249 {"\u2028", `\u2028`}, 250 {"\u2029", `\u2029`}, 251 {"\\", `\\`}, 252 {"\\n", `\\n`}, 253 {"foo\r\nbar", `foo\r\nbar`}, 254 // Preserve attribute boundaries. 255 {`"`, `\u0022`}, 256 {`'`, `\u0027`}, 257 // Allow embedding in HTML without further escaping. 258 {`&`, `\u0026amp;`}, 259 // Prevent breaking out of text node and element boundaries. 260 {"</script>", `\u003c\/script\u003e`}, 261 {"<![CDATA[", `\u003c!\[CDATA\[`}, 262 {"]]>", `\]\]\u003e`}, 263 // Escaping text spans. 264 {"<!--", `\u003c!\-\-`}, 265 {"-->", `\-\-\u003e`}, 266 {"*", `\*`}, 267 {"+", `\u002b`}, 268 {"?", `\?`}, 269 {"[](){}", `\[\]\(\)\{\}`}, 270 {"$foo|x.y", `\$foo\|x\.y`}, 271 {"x^y", `x\^y`}, 272 } 273 274 for _, test := range tests { 275 esc := jsRegexpEscaper(test.x) 276 if esc != test.esc { 277 t.Errorf("%q: want %q got %q", test.x, test.esc, esc) 278 } 279 } 280} 281 282func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { 283 input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 284 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 285 ` !"#$%&'()*+,-./` + 286 `0123456789:;<=>?` + 287 `@ABCDEFGHIJKLMNO` + 288 `PQRSTUVWXYZ[\]^_` + 289 "`abcdefghijklmno" + 290 "pqrstuvwxyz{|}~\x7f" + 291 "\u00A0\u0100\u2028\u2029\ufeff\U0001D11E") 292 293 tests := []struct { 294 name string 295 escaper func(...any) string 296 escaped string 297 }{ 298 { 299 "jsStrEscaper", 300 jsStrEscaper, 301 `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` + 302 `\u0008\t\n\u000b\f\r\u000e\u000f` + 303 `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` + 304 `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` + 305 ` !\u0022#$%\u0026\u0027()*\u002b,-.\/` + 306 `0123456789:;\u003c=\u003e?` + 307 `@ABCDEFGHIJKLMNO` + 308 `PQRSTUVWXYZ[\\]^_` + 309 "\\u0060abcdefghijklmno" + 310 "pqrstuvwxyz{|}~\u007f" + 311 "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", 312 }, 313 { 314 "jsRegexpEscaper", 315 jsRegexpEscaper, 316 `\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` + 317 `\u0008\t\n\u000b\f\r\u000e\u000f` + 318 `\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` + 319 `\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` + 320 ` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` + 321 `0123456789:;\u003c=\u003e\?` + 322 `@ABCDEFGHIJKLMNO` + 323 `PQRSTUVWXYZ\[\\\]\^_` + 324 "`abcdefghijklmno" + 325 `pqrstuvwxyz\{\|\}~` + "\u007f" + 326 "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E", 327 }, 328 } 329 330 for _, test := range tests { 331 if s := test.escaper(input); s != test.escaped { 332 t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) 333 continue 334 } 335 336 // Escape it rune by rune to make sure that any 337 // fast-path checking does not break escaping. 338 var buf strings.Builder 339 for _, c := range input { 340 buf.WriteString(test.escaper(string(c))) 341 } 342 343 if s := buf.String(); s != test.escaped { 344 t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s) 345 continue 346 } 347 } 348} 349 350func TestIsJsMimeType(t *testing.T) { 351 tests := []struct { 352 in string 353 out bool 354 }{ 355 {"application/javascript;version=1.8", true}, 356 {"application/javascript;version=1.8;foo=bar", true}, 357 {"application/javascript/version=1.8", false}, 358 {"text/javascript", true}, 359 {"application/json", true}, 360 {"application/ld+json", true}, 361 {"module", true}, 362 } 363 364 for _, test := range tests { 365 if isJSType(test.in) != test.out { 366 t.Errorf("isJSType(%q) = %v, want %v", test.in, !test.out, test.out) 367 } 368 } 369} 370 371func BenchmarkJSValEscaperWithNum(b *testing.B) { 372 for i := 0; i < b.N; i++ { 373 jsValEscaper(3.141592654) 374 } 375} 376 377func BenchmarkJSValEscaperWithStr(b *testing.B) { 378 for i := 0; i < b.N; i++ { 379 jsValEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") 380 } 381} 382 383func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) { 384 for i := 0; i < b.N; i++ { 385 jsValEscaper("The quick, brown fox jumps over the lazy dog") 386 } 387} 388 389func BenchmarkJSValEscaperWithObj(b *testing.B) { 390 o := struct { 391 S string 392 N int 393 }{ 394 "The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>\u2028", 395 42, 396 } 397 for i := 0; i < b.N; i++ { 398 jsValEscaper(o) 399 } 400} 401 402func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) { 403 o := struct { 404 S string 405 N int 406 }{ 407 "The quick, brown fox jumps over the lazy dog", 408 42, 409 } 410 for i := 0; i < b.N; i++ { 411 jsValEscaper(o) 412 } 413} 414 415func BenchmarkJSStrEscaperNoSpecials(b *testing.B) { 416 for i := 0; i < b.N; i++ { 417 jsStrEscaper("The quick, brown fox jumps over the lazy dog.") 418 } 419} 420 421func BenchmarkJSStrEscaper(b *testing.B) { 422 for i := 0; i < b.N; i++ { 423 jsStrEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") 424 } 425} 426 427func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) { 428 for i := 0; i < b.N; i++ { 429 jsRegexpEscaper("The quick, brown fox jumps over the lazy dog") 430 } 431} 432 433func BenchmarkJSRegexpEscaper(b *testing.B) { 434 for i := 0; i < b.N; i++ { 435 jsRegexpEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>") 436 } 437} 438