Commit ffd1c781 authored by Nodir Turakulov's avatar Nodir Turakulov Committed by Russ Cox

html/template: check "type" attribute in <script>

Currently any script tag is treated as a javascript container, although
<script type="text/template"> must not be. Check "type" attribute of
"script" tag. If it is present and it is not a JS MIME type, do not
transition to elementScript state.

Fixes #12149, where // inside text template was treated as regexp.
Fixes #6701

Change-Id: I8fc9e504f7280bdd800f40383c061853665ac8a2
Reviewed-on: https://go-review.googlesource.com/14336
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: 's avatarRuss Cox <rsc@golang.org>
parent f5516559
...@@ -161,6 +161,47 @@ func TestTypedContent(t *testing.T) { ...@@ -161,6 +161,47 @@ func TestTypedContent(t *testing.T) {
`greeting=H%69\x26addressee=(World)`, `greeting=H%69\x26addressee=(World)`,
}, },
}, },
{
`<script type="text/javascript">alert("{{.}}")</script>`,
[]string{
`\x3cb\x3e \x22foo%\x22 O\x27Reilly \x26bar;`,
`a[href =~ \x22\/\/example.com\x22]#foo`,
`Hello, \x3cb\x3eWorld\x3c\/b\x3e \x26amp;tc!`,
` dir=\x22ltr\x22`,
`c \x26\x26 alert(\x22Hello, World!\x22);`,
// Escape sequence not over-escaped.
`Hello, World \x26 O\x27Reilly\x21`,
`greeting=H%69\x26addressee=(World)`,
},
},
{
`<script type="text/javascript">alert({{.}})</script>`,
[]string{
`"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`,
`"a[href =~ \"//example.com\"]#foo"`,
`"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`,
`" dir=\"ltr\""`,
// Not escaped.
`c && alert("Hello, World!");`,
// Escape sequence not over-escaped.
`"Hello, World & O'Reilly\x21"`,
`"greeting=H%69\u0026addressee=(World)"`,
},
},
{
// Not treated as JS. The output is same as for <div>{{.}}</div>
`<script type="text/template">{{.}}</script>`,
[]string{
`&lt;b&gt; &#34;foo%&#34; O&#39;Reilly &amp;bar;`,
`a[href =~ &#34;//example.com&#34;]#foo`,
// Not escaped.
`Hello, <b>World</b> &amp;tc!`,
` dir=&#34;ltr&#34;`,
`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
`Hello, World &amp; O&#39;Reilly\x21`,
`greeting=H%69&amp;addressee=(World)`,
},
},
{ {
`<button onclick='alert("{{.}}")'>`, `<button onclick='alert("{{.}}")'>`,
[]string{ []string{
......
...@@ -285,7 +285,8 @@ type element uint8 ...@@ -285,7 +285,8 @@ type element uint8
const ( const (
// elementNone occurs outside a special tag or special element body. // elementNone occurs outside a special tag or special element body.
elementNone element = iota elementNone element = iota
// elementScript corresponds to the raw text <script> element. // elementScript corresponds to the raw text <script> element
// with JS MIME type or no type attribute.
elementScript elementScript
// elementStyle corresponds to the raw text <style> element. // elementStyle corresponds to the raw text <style> element.
elementStyle elementStyle
...@@ -319,6 +320,8 @@ const ( ...@@ -319,6 +320,8 @@ const (
attrNone attr = iota attrNone attr = iota
// attrScript corresponds to an event handler attribute. // attrScript corresponds to an event handler attribute.
attrScript attrScript
// attrScriptType corresponds to the type attribute in script HTML element
attrScriptType
// attrStyle corresponds to the style attribute whose value is CSS. // attrStyle corresponds to the style attribute whose value is CSS.
attrStyle attrStyle
// attrURL corresponds to an attribute whose value is a URL. // attrURL corresponds to an attribute whose value is a URL.
...@@ -326,10 +329,11 @@ const ( ...@@ -326,10 +329,11 @@ const (
) )
var attrNames = [...]string{ var attrNames = [...]string{
attrNone: "attrNone", attrNone: "attrNone",
attrScript: "attrScript", attrScript: "attrScript",
attrStyle: "attrStyle", attrScriptType: "attrScriptType",
attrURL: "attrURL", attrStyle: "attrStyle",
attrURL: "attrURL",
} }
func (a attr) String() string { func (a attr) String() string {
......
...@@ -673,6 +673,8 @@ func contextAfterText(c context, s []byte) (context, int) { ...@@ -673,6 +673,8 @@ func contextAfterText(c context, s []byte) (context, int) {
return transitionFunc[c.state](c, s[:i]) return transitionFunc[c.state](c, s[:i])
} }
// We are at the beginning of an attribute value.
i := bytes.IndexAny(s, delimEnds[c.delim]) i := bytes.IndexAny(s, delimEnds[c.delim])
if i == -1 { if i == -1 {
i = len(s) i = len(s)
...@@ -703,13 +705,21 @@ func contextAfterText(c context, s []byte) (context, int) { ...@@ -703,13 +705,21 @@ func contextAfterText(c context, s []byte) (context, int) {
} }
return c, len(s) return c, len(s)
} }
element := c.element
// If this is a non-JS "type" attribute inside "script" tag, do not treat the contents as JS.
if c.state == stateAttr && c.element == elementScript && c.attr == attrScriptType && !isJSType(string(s[:i])) {
element = elementNone
}
if c.delim != delimSpaceOrTagEnd { if c.delim != delimSpaceOrTagEnd {
// Consume any quote. // Consume any quote.
i++ i++
} }
// On exiting an attribute, we discard all state information // On exiting an attribute, we discard all state information
// except the state and element. // except the state and element.
return context{state: stateTag, element: c.element}, i return context{state: stateTag, element: element}, i
} }
// editActionNode records a change to an action pipeline for later commit. // editActionNode records a change to an action pipeline for later commit.
......
...@@ -1364,6 +1364,10 @@ func TestEscapeText(t *testing.T) { ...@@ -1364,6 +1364,10 @@ func TestEscapeText(t *testing.T) {
`<script type=text/javascript `, `<script type=text/javascript `,
context{state: stateTag, element: elementScript}, context{state: stateTag, element: elementScript},
}, },
{
`<script>`,
context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript},
},
{ {
`<script>foo`, `<script>foo`,
context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript}, context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},
...@@ -1388,6 +1392,14 @@ func TestEscapeText(t *testing.T) { ...@@ -1388,6 +1392,14 @@ func TestEscapeText(t *testing.T) {
`<script>document.write("<script>alert(1)</script>");`, `<script>document.write("<script>alert(1)</script>");`,
context{state: stateText}, context{state: stateText},
}, },
{
`<script type="text/template">`,
context{state: stateText},
},
{
`<script type="notjs">`,
context{state: stateText},
},
{ {
`<Script>`, `<Script>`,
context{state: stateJS, element: elementScript}, context{state: stateJS, element: elementScript},
......
...@@ -362,3 +362,41 @@ func isJSIdentPart(r rune) bool { ...@@ -362,3 +362,41 @@ func isJSIdentPart(r rune) bool {
} }
return false return false
} }
// isJSType returns true if the given MIME type should be considered JavaScript.
//
// It is used to determine whether a script tag with a type attribute is a javascript container.
func isJSType(mimeType string) bool {
// per
// http://www.w3.org/TR/html5/scripting-1.html#attr-script-type
// https://tools.ietf.org/html/rfc7231#section-3.1.1
// http://tools.ietf.org/html/rfc4329#section-3
// discard parameters
if i := strings.Index(mimeType, ";"); i >= 0 {
mimeType = mimeType[:i]
}
mimeType = strings.TrimSpace(mimeType)
switch mimeType {
case
"application/ecmascript",
"application/javascript",
"application/x-ecmascript",
"application/x-javascript",
"text/ecmascript",
"text/javascript",
"text/javascript1.0",
"text/javascript1.1",
"text/javascript1.2",
"text/javascript1.3",
"text/javascript1.4",
"text/javascript1.5",
"text/jscript",
"text/livescript",
"text/x-ecmascript",
"text/x-javascript":
return true
default:
return false
}
}
...@@ -332,6 +332,24 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { ...@@ -332,6 +332,24 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
} }
} }
func TestIsJsMimeType(t *testing.T) {
tests := []struct {
in string
out bool
}{
{"application/javascript;version=1.8", true},
{"application/javascript;version=1.8;foo=bar", true},
{"application/javascript/version=1.8", false},
{"text/javascript", true},
}
for _, test := range tests {
if isJSType(test.in) != test.out {
t.Errorf("isJSType(%q) = %v, want %v", test.in, !test.out, test.out)
}
}
}
func BenchmarkJSValEscaperWithNum(b *testing.B) { func BenchmarkJSValEscaperWithNum(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
jsValEscaper(3.141592654) jsValEscaper(3.141592654)
......
...@@ -105,14 +105,21 @@ func tTag(c context, s []byte) (context, int) { ...@@ -105,14 +105,21 @@ func tTag(c context, s []byte) (context, int) {
err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]), err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]),
}, len(s) }, len(s)
} }
switch attrType(string(s[i:j])) {
case contentTypeURL: attrName := string(s[i:j])
attr = attrURL if c.element == elementScript && attrName == "type" {
case contentTypeCSS: attr = attrScriptType
attr = attrStyle } else {
case contentTypeJS: switch attrType(attrName) {
attr = attrScript case contentTypeURL:
attr = attrURL
case contentTypeCSS:
attr = attrStyle
case contentTypeJS:
attr = attrScript
}
} }
if j == len(s) { if j == len(s) {
state = stateAttrName state = stateAttrName
} else { } else {
...@@ -149,10 +156,11 @@ func tAfterName(c context, s []byte) (context, int) { ...@@ -149,10 +156,11 @@ func tAfterName(c context, s []byte) (context, int) {
} }
var attrStartStates = [...]state{ var attrStartStates = [...]state{
attrNone: stateAttr, attrNone: stateAttr,
attrScript: stateJS, attrScript: stateJS,
attrStyle: stateCSS, attrScriptType: stateAttr,
attrURL: stateURL, attrStyle: stateCSS,
attrURL: stateURL,
} }
// tBeforeValue is the context transition function for stateBeforeValue. // tBeforeValue is the context transition function for stateBeforeValue.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment