html/template: fix escaping of URLs in meta content attributes

The WHATWG "shared declarative refresh steps" algorithm (§4.2.5.3)
skips ASCII whitespace between "url" and "=" when parsing the URL
portion of a meta content attribute.

Thank you to Samy Ghannad for reporting this issue.

Updates #78913
Fixes CVE-2026-39823

Change-Id: I7fc3bb9394b95e07b9b10fbc95725a3de6791774
Reviewed-on: https://go-review.googlesource.com/c/go/+/769920
Reviewed-by: Roland Shoemaker <roland@golang.org>
TryBot-Bypass: Roland Shoemaker <roland@golang.org>
This commit is contained in:
Neal Patel 2026-04-22 18:41:25 -04:00
parent 76c2c9b32a
commit f2ec1254ff
2 changed files with 27 additions and 5 deletions

View file

@ -760,6 +760,26 @@ func TestEscape(t *testing.T) {
`<meta http-equiv="refresh" content="{{"asd: 123"}}">`,
`<meta http-equiv="refresh" content="asd: 123">`,
},
{
"meta content url with whitespace before equals",
`<meta http-equiv="refresh" content="0;url ={{"javascript:alert(1)"}}">`,
`<meta http-equiv="refresh" content="0;url =#ZgotmplZ">`,
},
{
"meta content url with tab before equals",
"<meta http-equiv=\"refresh\" content=\"0;url\t={{\"javascript:alert(1)\"}}\">",
"<meta http-equiv=\"refresh\" content=\"0;url\t=#ZgotmplZ\">",
},
{
"meta content url with space after equals",
`<meta http-equiv="refresh" content="0;url= {{"javascript:alert(1)"}}">`,
`<meta http-equiv="refresh" content="0;url= #ZgotmplZ">`,
},
{
"meta content url with whitespace both sides of equals",
"<meta http-equiv=\"refresh\" content=\"0;url \t= {{\"javascript:alert(1)\"}}\">",
"<meta http-equiv=\"refresh\" content=\"0;url \t= #ZgotmplZ\">",
},
}
for _, test := range tests {

View file

@ -626,10 +626,12 @@ func tError(c context, s []byte) (context, int) {
// tMetaContent is the context transition function for the meta content attribute state.
func tMetaContent(c context, s []byte) (context, int) {
for i := 0; i < len(s); i++ {
if i+3 <= len(s)-1 && bytes.Equal(bytes.ToLower(s[i:i+4]), []byte("url=")) {
c.state = stateMetaContentURL
return c, i + 4
for i := range len(s) {
if i+3 <= len(s)-1 && bytes.EqualFold(s[i:i+3], []byte("url")) {
if j := eatWhiteSpace(s, i+3); j < len(s) && s[j] == '=' {
c.state = stateMetaContentURL
return c, j + 1
}
}
}
return c, len(s)
@ -637,7 +639,7 @@ func tMetaContent(c context, s []byte) (context, int) {
// tMetaContentURL is the context transition function for the "url=" part of a meta content attribute state.
func tMetaContentURL(c context, s []byte) (context, int) {
for i := 0; i < len(s); i++ {
for i := range len(s) {
if s[i] == ';' {
c.state = stateMetaContent
return c, i + 1