Skip to content

Commit 59706cd

Browse files
html: impose open element stack size limit
The HTML specification contains a number of algorithms which are quadratic in complexity by design. Instead of adding complicated workarounds to prevent these cases from becoming extremely expensive in pathological cases, we impose a limit of 512 to the size of the stack of open elements. It is extremely unlikely that non-adversarial HTML documents will ever hit this limit (but if we see cases of this, we may want to make the limit configurable via a ParseOption). Thanks to Guido Vranken and Jakub Ciolek for both independently reporting this issue. Fixes CVE-2025-47911 Fixes golang/go#75682 Change-Id: I890517b189af4ffbf427d25d3fde7ad7ec3509ad Reviewed-on: https://go-review.googlesource.com/c/net/+/709876 Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 6ec8895 commit 59706cd

File tree

3 files changed

+43
-5
lines changed

3 files changed

+43
-5
lines changed

html/escape.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func escape(w writer, s string) error {
299299
case '\r':
300300
esc = "&#13;"
301301
default:
302-
panic("unrecognized escape character")
302+
panic("html: unrecognized escape character")
303303
}
304304
s = s[i+1:]
305305
if _, err := w.WriteString(esc); err != nil {

html/parse.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,14 @@ func (p *parser) addChild(n *Node) {
231231
}
232232

233233
if n.Type == ElementNode {
234-
p.oe = append(p.oe, n)
234+
p.insertOpenElement(n)
235+
}
236+
}
237+
238+
func (p *parser) insertOpenElement(n *Node) {
239+
p.oe = append(p.oe, n)
240+
if len(p.oe) > 512 {
241+
panic("html: open stack of elements exceeds 512 nodes")
235242
}
236243
}
237244

@@ -810,7 +817,7 @@ func afterHeadIM(p *parser) bool {
810817
p.im = inFramesetIM
811818
return true
812819
case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Template, a.Title:
813-
p.oe = append(p.oe, p.head)
820+
p.insertOpenElement(p.head)
814821
defer p.oe.remove(p.head)
815822
return inHeadIM(p)
816823
case a.Head:
@@ -2324,9 +2331,13 @@ func (p *parser) parseCurrentToken() {
23242331
}
23252332
}
23262333

2327-
func (p *parser) parse() error {
2334+
func (p *parser) parse() (err error) {
2335+
defer func() {
2336+
if panicErr := recover(); panicErr != nil {
2337+
err = fmt.Errorf("%s", panicErr)
2338+
}
2339+
}()
23282340
// Iterate until EOF. Any other error will cause an early return.
2329-
var err error
23302341
for err != io.EOF {
23312342
// CDATA sections are allowed only in foreign content.
23322343
n := p.oe.top()
@@ -2355,6 +2366,8 @@ func (p *parser) parse() error {
23552366
// <tag>s. Conversely, explicit <tag>s in r's data can be silently dropped,
23562367
// with no corresponding node in the resulting tree.
23572368
//
2369+
// Parse will reject HTML that is nested deeper than 512 elements.
2370+
//
23582371
// The input is assumed to be UTF-8 encoded.
23592372
func Parse(r io.Reader) (*Node, error) {
23602373
return ParseWithOptions(r)

html/parse_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,3 +517,28 @@ func TestIssue70179(t *testing.T) {
517517
t.Fatalf("unexpected failure: %v", err)
518518
}
519519
}
520+
521+
func TestDepthLimit(t *testing.T) {
522+
for _, tc := range []struct {
523+
name string
524+
input string
525+
succeed bool
526+
}{
527+
// Not we don't use 512 as the limit here, because the parser will
528+
// insert implied <html> and <body> tags, increasing the size of the
529+
// stack by two before we start parsing the <dl>.
530+
{"above depth limit", strings.Repeat("<dl>", 511), false},
531+
{"below depth limit", strings.Repeat("<dl>", 510), true},
532+
{"above depth limit, interspersed elements", strings.Repeat("<dl><img />", 511), false},
533+
{"closing tags", strings.Repeat("</dl>", 512), true},
534+
} {
535+
t.Run(tc.name, func(t *testing.T) {
536+
_, err := Parse(strings.NewReader(tc.input))
537+
if tc.succeed && err != nil {
538+
t.Errorf("unexpected error: %v", err)
539+
} else if !tc.succeed && err == nil {
540+
t.Errorf("unexpected success")
541+
}
542+
})
543+
}
544+
}

0 commit comments

Comments
 (0)