Skip to content

Commit a452f3c

Browse files
rolandshoemakergopherbot
authored andcommitted
html: ignore duplicate attributes during tokenization
During tokenization ignore attributes with names we've already seen, per WHATWG 13.2.5.33. This removes a parser misalignment that could be leveraged to confuse sanitizers. Thanks to ensy for reporting this issue. Fixes CVE-2026-27136 Change-Id: Ib0a3edb8dbea35c431f74f8b0bbe6229625d7e1f Reviewed-on: https://go-review.googlesource.com/c/net/+/781685 Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-by: Nicholas Husin <nsh@golang.org> TryBot-Bypass: Roland Shoemaker <roland@golang.org> Auto-Submit: Gopher Robot <gobot@golang.org> Reviewed-by: Nicholas Husin <husin@google.com>
1 parent f865199 commit a452f3c

2 files changed

Lines changed: 19 additions & 4 deletions

File tree

html/token.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ type Tokenizer struct {
156156
// incremented on each call to TagAttr.
157157
pendingAttr [2]span
158158
attr [][2]span
159+
attrNames map[string]bool
159160
nAttrReturned int
160161
// rawTag is the "script" in "</script>" that closes the next token. If
161162
// non-empty, the subsequent call to Next will return a raw or RCDATA text
@@ -867,6 +868,7 @@ func (z *Tokenizer) readStartTag() TokenType {
867868
func (z *Tokenizer) readTag(saveAttr bool) {
868869
z.attr = z.attr[:0]
869870
z.nAttrReturned = 0
871+
clear(z.attrNames)
870872
// Read the tag name and attribute key/value pairs.
871873
z.readTagName()
872874
if z.skipWhiteSpace(); z.err != nil {
@@ -880,9 +882,11 @@ func (z *Tokenizer) readTag(saveAttr bool) {
880882
z.raw.end--
881883
z.readTagAttrKey()
882884
z.readTagAttrVal()
883-
// Save pendingAttr if saveAttr and that attribute has a non-empty key.
884-
if saveAttr && z.pendingAttr[0].start != z.pendingAttr[0].end {
885+
// Save pendingAttr if saveAttr and that attribute has a non-empty key, and the key hasn't been seen before.
886+
key := strings.ToLower(string(z.buf[z.pendingAttr[0].start:z.pendingAttr[0].end]))
887+
if saveAttr && z.pendingAttr[0].start != z.pendingAttr[0].end && !z.attrNames[key] {
885888
z.attr = append(z.attr, z.pendingAttr)
889+
z.attrNames[key] = true
886890
}
887891
if z.skipWhiteSpace(); z.err != nil {
888892
break
@@ -1273,8 +1277,9 @@ func NewTokenizer(r io.Reader) *Tokenizer {
12731277
// The input is assumed to be UTF-8 encoded.
12741278
func NewTokenizerFragment(r io.Reader, contextTag string) *Tokenizer {
12751279
z := &Tokenizer{
1276-
r: r,
1277-
buf: make([]byte, 0, 4096),
1280+
r: r,
1281+
buf: make([]byte, 0, 4096),
1282+
attrNames: make(map[string]bool),
12781283
}
12791284
if contextTag != "" {
12801285
switch s := strings.ToLower(contextTag); s {

html/token_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,16 @@ var tokenTests = []tokenTest{
626626
`<p a=/>`,
627627
`<p a="/">`,
628628
},
629+
{
630+
"duplicate attributes",
631+
`<p foo="bar" foo="baz">`,
632+
`<p foo="bar">`,
633+
},
634+
{
635+
"duplicate attributes, different case",
636+
`<p FOO="bar" foo="baz">`,
637+
`<p foo="bar">`,
638+
},
629639
}
630640

631641
func TestTokenizer(t *testing.T) {

0 commit comments

Comments
 (0)