From 59706cdaa8f95502fdec64b67b4c61d6ca58727d Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Mon, 29 Sep 2025 16:33:18 -0700 Subject: [PATCH] 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 LUCI-TryBot-Result: Go LUCI --- html/escape.go | 2 +- html/parse.go | 21 +++++++++++++++++---- html/parse_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/html/escape.go b/html/escape.go index 04c6bec2..12f22737 100644 --- a/html/escape.go +++ b/html/escape.go @@ -299,7 +299,7 @@ func escape(w writer, s string) error { case '\r': esc = " " default: - panic("unrecognized escape character") + panic("html: unrecognized escape character") } s = s[i+1:] if _, err := w.WriteString(esc); err != nil { diff --git a/html/parse.go b/html/parse.go index 722e9277..88fc0056 100644 --- a/html/parse.go +++ b/html/parse.go @@ -231,7 +231,14 @@ func (p *parser) addChild(n *Node) { } if n.Type == ElementNode { - p.oe = append(p.oe, n) + p.insertOpenElement(n) + } +} + +func (p *parser) insertOpenElement(n *Node) { + p.oe = append(p.oe, n) + if len(p.oe) > 512 { + panic("html: open stack of elements exceeds 512 nodes") } } @@ -810,7 +817,7 @@ func afterHeadIM(p *parser) bool { p.im = inFramesetIM return true case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Template, a.Title: - p.oe = append(p.oe, p.head) + p.insertOpenElement(p.head) defer p.oe.remove(p.head) return inHeadIM(p) case a.Head: @@ -2324,9 +2331,13 @@ func (p *parser) parseCurrentToken() { } } -func (p *parser) parse() error { +func (p *parser) parse() (err error) { + defer func() { + if panicErr := recover(); panicErr != nil { + err = fmt.Errorf("%s", panicErr) + } + }() // Iterate until EOF. Any other error will cause an early return. - var err error for err != io.EOF { // CDATA sections are allowed only in foreign content. n := p.oe.top() @@ -2355,6 +2366,8 @@ func (p *parser) parse() error { // s. Conversely, explicit s in r's data can be silently dropped, // with no corresponding node in the resulting tree. // +// Parse will reject HTML that is nested deeper than 512 elements. +// // The input is assumed to be UTF-8 encoded. func Parse(r io.Reader) (*Node, error) { return ParseWithOptions(r) diff --git a/html/parse_test.go b/html/parse_test.go index ed9e9155..fe66eb44 100644 --- a/html/parse_test.go +++ b/html/parse_test.go @@ -517,3 +517,28 @@ func TestIssue70179(t *testing.T) { t.Fatalf("unexpected failure: %v", err) } } + +func TestDepthLimit(t *testing.T) { + for _, tc := range []struct { + name string + input string + succeed bool + }{ + // Not we don't use 512 as the limit here, because the parser will + // insert implied and tags, increasing the size of the + // stack by two before we start parsing the
. + {"above depth limit", strings.Repeat("
", 511), false}, + {"below depth limit", strings.Repeat("
", 510), true}, + {"above depth limit, interspersed elements", strings.Repeat("
", 511), false}, + {"closing tags", strings.Repeat("
", 512), true}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := Parse(strings.NewReader(tc.input)) + if tc.succeed && err != nil { + t.Errorf("unexpected error: %v", err) + } else if !tc.succeed && err == nil { + t.Errorf("unexpected success") + } + }) + } +}