[release-branch.go1.20] html/template: support HTML-like comments in script contexts

Per Appendix B.1.1 of the ECMAScript specification, support HTML-like
comments in script contexts. Also per section 12.5, support hashbang
comments. This brings our parsing in-line with how browsers treat these
comment types.

Thanks to Takeshi Kaneko (GMO Cybersecurity by Ierae, Inc.) for
reporting this issue.

Fixes #62196
Fixes #62395
Fixes CVE-2023-39318

Change-Id: Id512702c5de3ae46cf648e268cb10e1eb392a181
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1976593
Run-TryBot: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2014620
Reviewed-on: https://go-review.googlesource.com/c/go/+/526098
Run-TryBot: Cherry Mui <cherryyz@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Roland Shoemaker 2023-08-03 12:24:13 -07:00 committed by Cherry Mui
parent 612da32fb5
commit 023b542edf
5 changed files with 84 additions and 43 deletions

View File

@ -128,6 +128,10 @@ const (
stateJSBlockCmt stateJSBlockCmt
// stateJSLineCmt occurs inside a JavaScript // line comment. // stateJSLineCmt occurs inside a JavaScript // line comment.
stateJSLineCmt stateJSLineCmt
// stateJSHTMLOpenCmt occurs inside a JavaScript <!-- HTML-like comment.
stateJSHTMLOpenCmt
// stateJSHTMLCloseCmt occurs inside a JavaScript --> HTML-like comment.
stateJSHTMLCloseCmt
// stateCSS occurs inside a <style> element or style attribute. // stateCSS occurs inside a <style> element or style attribute.
stateCSS stateCSS
// stateCSSDqStr occurs inside a CSS double quoted string. // stateCSSDqStr occurs inside a CSS double quoted string.
@ -155,7 +159,7 @@ const (
// authors & maintainers, not for end-users or machines. // authors & maintainers, not for end-users or machines.
func isComment(s state) bool { func isComment(s state) bool {
switch s { switch s {
case stateHTMLCmt, stateJSBlockCmt, stateJSLineCmt, stateCSSBlockCmt, stateCSSLineCmt: case stateHTMLCmt, stateJSBlockCmt, stateJSLineCmt, stateJSHTMLOpenCmt, stateJSHTMLCloseCmt, stateCSSBlockCmt, stateCSSLineCmt:
return true return true
} }
return false return false

View File

@ -776,9 +776,12 @@ func (e *escaper) escapeText(c context, n *parse.TextNode) context {
if c.state != c1.state && isComment(c1.state) && c1.delim == delimNone { if c.state != c1.state && isComment(c1.state) && c1.delim == delimNone {
// Preserve the portion between written and the comment start. // Preserve the portion between written and the comment start.
cs := i1 - 2 cs := i1 - 2
if c1.state == stateHTMLCmt { if c1.state == stateHTMLCmt || c1.state == stateJSHTMLOpenCmt {
// "<!--" instead of "/*" or "//" // "<!--" instead of "/*" or "//"
cs -= 2 cs -= 2
} else if c1.state == stateJSHTMLCloseCmt {
// "-->" instead of "/*" or "//"
cs -= 1
} }
b.Write(s[written:cs]) b.Write(s[written:cs])
written = i1 written = i1

View File

@ -503,6 +503,16 @@ func TestEscape(t *testing.T) {
"<script>var a/*b*///c\nd</script>", "<script>var a/*b*///c\nd</script>",
"<script>var a \nd</script>", "<script>var a \nd</script>",
}, },
{
"JS HTML-like comments",
"<script>before <!-- beep\nbetween\nbefore-->boop\n</script>",
"<script>before \nbetween\nbefore\n</script>",
},
{
"JS hashbang comment",
"<script>#! beep\n</script>",
"<script>\n</script>",
},
{ {
"CSS comments", "CSS comments",
"<style>p// paragraph\n" + "<style>p// paragraph\n" +

View File

@ -25,21 +25,23 @@ func _() {
_ = x[stateJSRegexp-14] _ = x[stateJSRegexp-14]
_ = x[stateJSBlockCmt-15] _ = x[stateJSBlockCmt-15]
_ = x[stateJSLineCmt-16] _ = x[stateJSLineCmt-16]
_ = x[stateCSS-17] _ = x[stateJSHTMLOpenCmt-17]
_ = x[stateCSSDqStr-18] _ = x[stateJSHTMLCloseCmt-18]
_ = x[stateCSSSqStr-19] _ = x[stateCSS-19]
_ = x[stateCSSDqURL-20] _ = x[stateCSSDqStr-20]
_ = x[stateCSSSqURL-21] _ = x[stateCSSSqStr-21]
_ = x[stateCSSURL-22] _ = x[stateCSSDqURL-22]
_ = x[stateCSSBlockCmt-23] _ = x[stateCSSSqURL-23]
_ = x[stateCSSLineCmt-24] _ = x[stateCSSURL-24]
_ = x[stateError-25] _ = x[stateCSSBlockCmt-25]
_ = x[stateDead-26] _ = x[stateCSSLineCmt-26]
_ = x[stateError-27]
_ = x[stateDead-28]
} }
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead" const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 204, 217, 230, 243, 256, 267, 283, 298, 308, 317} var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 214, 233, 241, 254, 267, 280, 293, 304, 320, 335, 345, 354}
func (i state) String() string { func (i state) String() string {
if i >= state(len(_state_index)-1) { if i >= state(len(_state_index)-1) {

View File

@ -31,6 +31,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){
stateJSRegexp: tJSDelimited, stateJSRegexp: tJSDelimited,
stateJSBlockCmt: tBlockCmt, stateJSBlockCmt: tBlockCmt,
stateJSLineCmt: tLineCmt, stateJSLineCmt: tLineCmt,
stateJSHTMLOpenCmt: tLineCmt,
stateJSHTMLCloseCmt: tLineCmt,
stateCSS: tCSS, stateCSS: tCSS,
stateCSSDqStr: tCSSStr, stateCSSDqStr: tCSSStr,
stateCSSSqStr: tCSSStr, stateCSSSqStr: tCSSStr,
@ -263,7 +265,7 @@ func tURL(c context, s []byte) (context, int) {
// tJS is the context transition function for the JS state. // tJS is the context transition function for the JS state.
func tJS(c context, s []byte) (context, int) { func tJS(c context, s []byte) (context, int) {
i := bytes.IndexAny(s, "\"`'/") i := bytes.IndexAny(s, "\"`'/<-#")
if i == -1 { if i == -1 {
// Entire input is non string, comment, regexp tokens. // Entire input is non string, comment, regexp tokens.
c.jsCtx = nextJSCtx(s, c.jsCtx) c.jsCtx = nextJSCtx(s, c.jsCtx)
@ -293,6 +295,26 @@ func tJS(c context, s []byte) (context, int) {
err: errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]), err: errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]),
}, len(s) }, len(s)
} }
// ECMAScript supports HTML style comments for legacy reasons, see Appendix
// B.1.1 "HTML-like Comments". The handling of these comments is somewhat
// confusing. Multi-line comments are not supported, i.e. anything on lines
// between the opening and closing tokens is not considered a comment, but
// anything following the opening or closing token, on the same line, is
// ignored. As such we simply treat any line prefixed with "<!--" or "-->"
// as if it were actually prefixed with "//" and move on.
case '<':
if i+3 < len(s) && bytes.Equal(commentStart, s[i:i+4]) {
c.state, i = stateJSHTMLOpenCmt, i+3
}
case '-':
if i+2 < len(s) && bytes.Equal(commentEnd, s[i:i+3]) {
c.state, i = stateJSHTMLCloseCmt, i+2
}
// ECMAScript also supports "hashbang" comment lines, see Section 12.5.
case '#':
if i+1 < len(s) && s[i+1] == '!' {
c.state, i = stateJSLineCmt, i+1
}
default: default:
panic("unreachable") panic("unreachable")
} }
@ -372,12 +394,12 @@ func tBlockCmt(c context, s []byte) (context, int) {
return c, i + 2 return c, i + 2
} }
// tLineCmt is the context transition function for //comment states. // tLineCmt is the context transition function for //comment states, and the JS HTML-like comment state.
func tLineCmt(c context, s []byte) (context, int) { func tLineCmt(c context, s []byte) (context, int) {
var lineTerminators string var lineTerminators string
var endState state var endState state
switch c.state { switch c.state {
case stateJSLineCmt: case stateJSLineCmt, stateJSHTMLOpenCmt, stateJSHTMLCloseCmt:
lineTerminators, endState = "\n\r\u2028\u2029", stateJS lineTerminators, endState = "\n\r\u2028\u2029", stateJS
case stateCSSLineCmt: case stateCSSLineCmt:
lineTerminators, endState = "\n\f\r", stateCSS lineTerminators, endState = "\n\f\r", stateCSS