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: Russ Cox <rsc@golang.org>
This commit is contained in:
Nodir Turakulov 2015-09-05 06:38:13 -07:00 committed by Russ Cox
parent f5516559e6
commit ffd1c781b7
7 changed files with 148 additions and 17 deletions

View file

@ -161,6 +161,47 @@ func TestTypedContent(t *testing.T) {
`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("{{.}}")'>`,
[]string{

View file

@ -285,7 +285,8 @@ type element uint8
const (
// elementNone occurs outside a special tag or special element body.
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
// elementStyle corresponds to the raw text <style> element.
elementStyle
@ -319,6 +320,8 @@ const (
attrNone attr = iota
// attrScript corresponds to an event handler attribute.
attrScript
// attrScriptType corresponds to the type attribute in script HTML element
attrScriptType
// attrStyle corresponds to the style attribute whose value is CSS.
attrStyle
// attrURL corresponds to an attribute whose value is a URL.
@ -326,10 +329,11 @@ const (
)
var attrNames = [...]string{
attrNone: "attrNone",
attrScript: "attrScript",
attrStyle: "attrStyle",
attrURL: "attrURL",
attrNone: "attrNone",
attrScript: "attrScript",
attrScriptType: "attrScriptType",
attrStyle: "attrStyle",
attrURL: "attrURL",
}
func (a attr) String() string {

View file

@ -673,6 +673,8 @@ func contextAfterText(c context, s []byte) (context, int) {
return transitionFunc[c.state](c, s[:i])
}
// We are at the beginning of an attribute value.
i := bytes.IndexAny(s, delimEnds[c.delim])
if i == -1 {
i = len(s)
@ -703,13 +705,21 @@ func contextAfterText(c context, s []byte) (context, int) {
}
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 {
// Consume any quote.
i++
}
// On exiting an attribute, we discard all state information
// 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.

View file

@ -1364,6 +1364,10 @@ func TestEscapeText(t *testing.T) {
`<script type=text/javascript `,
context{state: stateTag, element: elementScript},
},
{
`<script>`,
context{state: stateJS, jsCtx: jsCtxRegexp, element: elementScript},
},
{
`<script>foo`,
context{state: stateJS, jsCtx: jsCtxDivOp, element: elementScript},
@ -1388,6 +1392,14 @@ func TestEscapeText(t *testing.T) {
`<script>document.write("<script>alert(1)</script>");`,
context{state: stateText},
},
{
`<script type="text/template">`,
context{state: stateText},
},
{
`<script type="notjs">`,
context{state: stateText},
},
{
`<Script>`,
context{state: stateJS, element: elementScript},

View file

@ -362,3 +362,41 @@ func isJSIdentPart(r rune) bool {
}
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
}
}

View file

@ -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) {
for i := 0; i < b.N; i++ {
jsValEscaper(3.141592654)

View file

@ -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:]),
}, len(s)
}
switch attrType(string(s[i:j])) {
case contentTypeURL:
attr = attrURL
case contentTypeCSS:
attr = attrStyle
case contentTypeJS:
attr = attrScript
attrName := string(s[i:j])
if c.element == elementScript && attrName == "type" {
attr = attrScriptType
} else {
switch attrType(attrName) {
case contentTypeURL:
attr = attrURL
case contentTypeCSS:
attr = attrStyle
case contentTypeJS:
attr = attrScript
}
}
if j == len(s) {
state = stateAttrName
} else {
@ -149,10 +156,11 @@ func tAfterName(c context, s []byte) (context, int) {
}
var attrStartStates = [...]state{
attrNone: stateAttr,
attrScript: stateJS,
attrStyle: stateCSS,
attrURL: stateURL,
attrNone: stateAttr,
attrScript: stateJS,
attrScriptType: stateAttr,
attrStyle: stateCSS,
attrURL: stateURL,
}
// tBeforeValue is the context transition function for stateBeforeValue.