Source file src/html/template/js_test.go

     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  
     5  package template
     6  
     7  import (
     8  	"errors"
     9  	"math"
    10  	"strings"
    11  	"testing"
    12  )
    13  
    14  func 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  		{jsCtxRegexp, "yield"},
    69  		// A keyword after property access is a property name.
    70  		{jsCtxDivOp, "x.yield"},
    71  		{jsCtxDivOp, "x?.yield"},
    72  		{jsCtxDivOp, "x.\nyield"},
    73  		{jsCtxDivOp, "x.in"},
    74  		// Identifiers can be divided and cannot validly be preceded by
    75  		// a regular expressions. Semicolon insertion cannot happen
    76  		// between an identifier and a regular expression on a new line
    77  		// because the one token lookahead for semicolon insertion has
    78  		// to conclude that it could be a div binary op and treat it as
    79  		// such.
    80  		{jsCtxDivOp, "x"},
    81  		{jsCtxDivOp, "x "},
    82  		{jsCtxDivOp, "x\t"},
    83  		{jsCtxDivOp, "x\n"},
    84  		{jsCtxDivOp, "x\u2028"},
    85  		{jsCtxDivOp, "preturn"},
    86  		// Numbers precede div ops.
    87  		{jsCtxDivOp, "0"},
    88  		// Dots that are part of a number are div preceders.
    89  		{jsCtxDivOp, "0."},
    90  		// Some JS interpreters treat NBSP as a normal space, so
    91  		// we must too in order to properly escape things.
    92  		{jsCtxRegexp, "=\u00A0"},
    93  	}
    94  
    95  	for _, test := range tests {
    96  		if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx {
    97  			t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
    98  		}
    99  		if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx {
   100  			t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
   101  		}
   102  	}
   103  
   104  	if nextJSCtx([]byte("   "), jsCtxRegexp) != jsCtxRegexp {
   105  		t.Error("Blank tokens")
   106  	}
   107  
   108  	if nextJSCtx([]byte("   "), jsCtxDivOp) != jsCtxDivOp {
   109  		t.Error("Blank tokens")
   110  	}
   111  }
   112  
   113  type jsonErrType struct{}
   114  
   115  func (e *jsonErrType) MarshalJSON() ([]byte, error) {
   116  	return nil, errors.New("a */ b <script c </script d <!-- e <sCrIpT f </sCrIpT")
   117  }
   118  
   119  func TestJSValEscaper(t *testing.T) {
   120  	tests := []struct {
   121  		x        any
   122  		js       string
   123  		skipNest bool
   124  	}{
   125  		{int(42), " 42 ", false},
   126  		{uint(42), " 42 ", false},
   127  		{int16(42), " 42 ", false},
   128  		{uint16(42), " 42 ", false},
   129  		{int32(-42), " -42 ", false},
   130  		{uint32(42), " 42 ", false},
   131  		{int16(-42), " -42 ", false},
   132  		{uint16(42), " 42 ", false},
   133  		{int64(-42), " -42 ", false},
   134  		{uint64(42), " 42 ", false},
   135  		{uint64(1) << 53, " 9007199254740992 ", false},
   136  		// ulp(1 << 53) > 1 so this loses precision in JS
   137  		// but it is still a representable integer literal.
   138  		{uint64(1)<<53 + 1, " 9007199254740993 ", false},
   139  		{float32(1.0), " 1 ", false},
   140  		{float32(-1.0), " -1 ", false},
   141  		{float32(0.5), " 0.5 ", false},
   142  		{float32(-0.5), " -0.5 ", false},
   143  		{float32(1.0) / float32(256), " 0.00390625 ", false},
   144  		{float32(0), " 0 ", false},
   145  		{math.Copysign(0, -1), " -0 ", false},
   146  		{float64(1.0), " 1 ", false},
   147  		{float64(-1.0), " -1 ", false},
   148  		{float64(0.5), " 0.5 ", false},
   149  		{float64(-0.5), " -0.5 ", false},
   150  		{float64(0), " 0 ", false},
   151  		{math.Copysign(0, -1), " -0 ", false},
   152  		{"", `""`, false},
   153  		{"foo", `"foo"`, false},
   154  		// Newlines.
   155  		{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`, false},
   156  		// "\v" == "v" on IE 6 so use "\u000b" instead.
   157  		{"\t\x0b", `"\t\u000b"`, false},
   158  		{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`, false},
   159  		{[]any{}, "[]", false},
   160  		{[]any{42, "foo", nil}, `[42,"foo",null]`, false},
   161  		{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`, false},
   162  		{"<!--", `"\u003c!--"`, false},
   163  		{"-->", `"--\u003e"`, false},
   164  		{"<![CDATA[", `"\u003c![CDATA["`, false},
   165  		{"]]>", `"]]\u003e"`, false},
   166  		{"</script", `"\u003c/script"`, false},
   167  		{"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
   168  		{nil, " null ", false},
   169  		{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: a * / b \\x3Cscript c \\x3C/script d \\x3C!-- e \\x3Cscript f \\x3C/script */null ", true},
   170  	}
   171  
   172  	for _, test := range tests {
   173  		if js := jsValEscaper(test.x); js != test.js {
   174  			t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js)
   175  		}
   176  		if test.skipNest {
   177  			continue
   178  		}
   179  		// Make sure that escaping corner cases are not broken
   180  		// by nesting.
   181  		a := []any{test.x}
   182  		want := "[" + strings.TrimSpace(test.js) + "]"
   183  		if js := jsValEscaper(a); js != want {
   184  			t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js)
   185  		}
   186  	}
   187  }
   188  
   189  func TestJSStrEscaper(t *testing.T) {
   190  	tests := []struct {
   191  		x   any
   192  		esc string
   193  	}{
   194  		{"", ``},
   195  		{"foo", `foo`},
   196  		{"\u0000", `\u0000`},
   197  		{"\t", `\t`},
   198  		{"\n", `\n`},
   199  		{"\r", `\r`},
   200  		{"\u2028", `\u2028`},
   201  		{"\u2029", `\u2029`},
   202  		{"\\", `\\`},
   203  		{"\\n", `\\n`},
   204  		{"foo\r\nbar", `foo\r\nbar`},
   205  		// Preserve attribute boundaries.
   206  		{`"`, `\u0022`},
   207  		{`'`, `\u0027`},
   208  		// Allow embedding in HTML without further escaping.
   209  		{`&amp;`, `\u0026amp;`},
   210  		// Prevent breaking out of text node and element boundaries.
   211  		{"</script>", `\u003c\/script\u003e`},
   212  		{"<![CDATA[", `\u003c![CDATA[`},
   213  		{"]]>", `]]\u003e`},
   214  		// https://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
   215  		//   "The text in style, script, title, and textarea elements
   216  		//   must not have an escaping text span start that is not
   217  		//   followed by an escaping text span end."
   218  		// Furthermore, spoofing an escaping text span end could lead
   219  		// to different interpretation of a </script> sequence otherwise
   220  		// masked by the escaping text span, and spoofing a start could
   221  		// allow regular text content to be interpreted as script
   222  		// allowing script execution via a combination of a JS string
   223  		// injection followed by an HTML text injection.
   224  		{"<!--", `\u003c!--`},
   225  		{"-->", `--\u003e`},
   226  		// From https://code.google.com/p/doctype/wiki/ArticleUtf7
   227  		{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
   228  			`\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
   229  		},
   230  		// Invalid UTF-8 sequence
   231  		{"foo\xA0bar", "foo\xA0bar"},
   232  		// Invalid unicode scalar value.
   233  		{"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"},
   234  	}
   235  
   236  	for _, test := range tests {
   237  		esc := jsStrEscaper(test.x)
   238  		if esc != test.esc {
   239  			t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
   240  		}
   241  	}
   242  }
   243  
   244  func TestJSRegexpEscaper(t *testing.T) {
   245  	tests := []struct {
   246  		x   any
   247  		esc string
   248  	}{
   249  		{"", `(?:)`},
   250  		{"foo", `foo`},
   251  		{"\u0000", `\u0000`},
   252  		{"\t", `\t`},
   253  		{"\n", `\n`},
   254  		{"\r", `\r`},
   255  		{"\u2028", `\u2028`},
   256  		{"\u2029", `\u2029`},
   257  		{"\\", `\\`},
   258  		{"\\n", `\\n`},
   259  		{"foo\r\nbar", `foo\r\nbar`},
   260  		// Preserve attribute boundaries.
   261  		{`"`, `\u0022`},
   262  		{`'`, `\u0027`},
   263  		// Allow embedding in HTML without further escaping.
   264  		{`&amp;`, `\u0026amp;`},
   265  		// Prevent breaking out of text node and element boundaries.
   266  		{"</script>", `\u003c\/script\u003e`},
   267  		{"<![CDATA[", `\u003c!\[CDATA\[`},
   268  		{"]]>", `\]\]\u003e`},
   269  		// Escaping text spans.
   270  		{"<!--", `\u003c!\-\-`},
   271  		{"-->", `\-\-\u003e`},
   272  		{"*", `\*`},
   273  		{"+", `\u002b`},
   274  		{"?", `\?`},
   275  		{"[](){}", `\[\]\(\)\{\}`},
   276  		{"$foo|x.y", `\$foo\|x\.y`},
   277  		{"x^y", `x\^y`},
   278  	}
   279  
   280  	for _, test := range tests {
   281  		esc := jsRegexpEscaper(test.x)
   282  		if esc != test.esc {
   283  			t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
   284  		}
   285  	}
   286  }
   287  
   288  func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
   289  	input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
   290  		"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
   291  		` !"#$%&'()*+,-./` +
   292  		`0123456789:;<=>?` +
   293  		`@ABCDEFGHIJKLMNO` +
   294  		`PQRSTUVWXYZ[\]^_` +
   295  		"`abcdefghijklmno" +
   296  		"pqrstuvwxyz{|}~\x7f" +
   297  		"\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
   298  
   299  	tests := []struct {
   300  		name    string
   301  		escaper func(...any) string
   302  		escaped string
   303  	}{
   304  		{
   305  			"jsStrEscaper",
   306  			jsStrEscaper,
   307  			`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
   308  				`\u0008\t\n\u000b\f\r\u000e\u000f` +
   309  				`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
   310  				`\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
   311  				` !\u0022#$%\u0026\u0027()*\u002b,-.\/` +
   312  				`0123456789:;\u003c=\u003e?` +
   313  				`@ABCDEFGHIJKLMNO` +
   314  				`PQRSTUVWXYZ[\\]^_` +
   315  				"\\u0060abcdefghijklmno" +
   316  				"pqrstuvwxyz{|}~\u007f" +
   317  				"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
   318  		},
   319  		{
   320  			"jsRegexpEscaper",
   321  			jsRegexpEscaper,
   322  			`\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007` +
   323  				`\u0008\t\n\u000b\f\r\u000e\u000f` +
   324  				`\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017` +
   325  				`\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f` +
   326  				` !\u0022#\$%\u0026\u0027\(\)\*\u002b,\-\.\/` +
   327  				`0123456789:;\u003c=\u003e\?` +
   328  				`@ABCDEFGHIJKLMNO` +
   329  				`PQRSTUVWXYZ\[\\\]\^_` +
   330  				"`abcdefghijklmno" +
   331  				`pqrstuvwxyz\{\|\}~` + "\u007f" +
   332  				"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
   333  		},
   334  	}
   335  
   336  	for _, test := range tests {
   337  		if s := test.escaper(input); s != test.escaped {
   338  			t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
   339  			continue
   340  		}
   341  
   342  		// Escape it rune by rune to make sure that any
   343  		// fast-path checking does not break escaping.
   344  		var buf strings.Builder
   345  		for _, c := range input {
   346  			buf.WriteString(test.escaper(string(c)))
   347  		}
   348  
   349  		if s := buf.String(); s != test.escaped {
   350  			t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
   351  			continue
   352  		}
   353  	}
   354  }
   355  
   356  func TestIsJsMimeType(t *testing.T) {
   357  	tests := []struct {
   358  		in  string
   359  		out bool
   360  	}{
   361  		{"application/javascript;version=1.8", true},
   362  		{"application/javascript;version=1.8;foo=bar", true},
   363  		{"application/javascript/version=1.8", false},
   364  		{"text/javascript", true},
   365  		{"application/json", true},
   366  		{"application/ld+json", true},
   367  		{"module", true},
   368  	}
   369  
   370  	for _, test := range tests {
   371  		if isJSType(test.in) != test.out {
   372  			t.Errorf("isJSType(%q) = %v, want %v", test.in, !test.out, test.out)
   373  		}
   374  	}
   375  }
   376  
   377  func BenchmarkJSValEscaperWithNum(b *testing.B) {
   378  	for i := 0; i < b.N; i++ {
   379  		jsValEscaper(3.141592654)
   380  	}
   381  }
   382  
   383  func BenchmarkJSValEscaperWithStr(b *testing.B) {
   384  	for i := 0; i < b.N; i++ {
   385  		jsValEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
   386  	}
   387  }
   388  
   389  func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) {
   390  	for i := 0; i < b.N; i++ {
   391  		jsValEscaper("The quick, brown fox jumps over the lazy dog")
   392  	}
   393  }
   394  
   395  func BenchmarkJSValEscaperWithObj(b *testing.B) {
   396  	o := struct {
   397  		S string
   398  		N int
   399  	}{
   400  		"The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>\u2028",
   401  		42,
   402  	}
   403  	for i := 0; i < b.N; i++ {
   404  		jsValEscaper(o)
   405  	}
   406  }
   407  
   408  func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) {
   409  	o := struct {
   410  		S string
   411  		N int
   412  	}{
   413  		"The quick, brown fox jumps over the lazy dog",
   414  		42,
   415  	}
   416  	for i := 0; i < b.N; i++ {
   417  		jsValEscaper(o)
   418  	}
   419  }
   420  
   421  func BenchmarkJSStrEscaperNoSpecials(b *testing.B) {
   422  	for i := 0; i < b.N; i++ {
   423  		jsStrEscaper("The quick, brown fox jumps over the lazy dog.")
   424  	}
   425  }
   426  
   427  func BenchmarkJSStrEscaper(b *testing.B) {
   428  	for i := 0; i < b.N; i++ {
   429  		jsStrEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
   430  	}
   431  }
   432  
   433  func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) {
   434  	for i := 0; i < b.N; i++ {
   435  		jsRegexpEscaper("The quick, brown fox jumps over the lazy dog")
   436  	}
   437  }
   438  
   439  func BenchmarkJSRegexpEscaper(b *testing.B) {
   440  	for i := 0; i < b.N; i++ {
   441  		jsRegexpEscaper("The <i>quick</i>,\r\n<span style='color:brown'>brown</span> fox jumps\u2028over the <canine class=\"lazy\">dog</canine>")
   442  	}
   443  }
   444  

View as plain text