Death by a Thousand Tags: The Quadratic HTML DoS in Go
Feb 13, 2026·7 min read·16 visits
Executive Summary (TL;DR)
The Go HTML parser (`x/net/html`) contains a quadratic complexity bug in its tree construction logic. An attacker can send a malicious HTML payload (like deeply nested or malformed tags) that causes the parser to consume excessive CPU resources, effectively hanging the application. Fix: Upgrade `golang.org/x/net` to v0.45.0.
In the world of safe memory languages, we often forget that algorithmic complexity is a vulnerability class of its own. CVE-2025-47911 serves as a stark reminder: you don't need a buffer overflow to kill a server; you just need a really annoying HTML table. This vulnerability affects the `golang.org/x/net/html` package—the de facto standard for HTML parsing in the Go ecosystem—allowing attackers to trigger quadratic time complexity ($O(n^2)$) during the parsing of specially crafted inputs.
The Hook: When Parsing Goes Wrong
We've all been there. You need to parse some HTML. Maybe you're building a web crawler, a link preview generator, or an email sanitizer. You know better than to use Regex (because Zalgo comes), so you reach for the gold standard: golang.org/x/net/html. It's robust, it's maintained by the Go team, and it implements the HTML5 specification. What could go wrong?
Here is the thing about specifications: the HTML5 spec is a nightmare of backward compatibility and error recovery. It is designed to handle the absolute garbage markup that the internet produced in the 90s. This means the parser has to be smart. It has to fix your mistakes. It has to re-parent elements, close unclosed tags, and basically guess what the developer meant.
CVE-2025-47911 is what happens when that helpfulness turns toxic. By feeding the parser a specifically crafted sequence of garbage—think thousands of misaligned select or table tags—we can force the parser into a "panic mode" of sorts. It doesn't crash (panic) in the Go sense; instead, it enters a state of algorithmic despair, frantically walking up and down the element stack, consuming CPU cycles like they're free candy. It's a classic Algorithmic Complexity Denial of Service, and it turns your shiny Go microservice into a brick.
The Flaw: The Adoption Agency Algorithm
To understand this bug, you have to understand one of the most cursed parts of the HTML5 spec: the Adoption Agency Algorithm. This logic handles cases where formatting elements (like b, i, font) are closed in the wrong order or interrupted by block elements. For example, if you write <b><p>Text</b></p>, the browser (and the Go parser) has to figure out how to close the b tag before the p starts, or extend it into the p.
In the x/net/html implementation, the parser maintains a stack of open elements. When it encounters certain tags in specific states, it has to iterate through this stack to find a matching formatting element or to perform "fostering" of orphan elements (common in table parsing).
The vulnerability arises because the code failed to cap the number of iterations required for these stack fix-ups relative to the input size. If I send you an HTML document with 10,000 nested formatting elements interlaced with table elements, the parser might have to scan a significant portion of that 10,000-deep stack for every new tag I insert.
Mathematically, we are looking at $O(n^2)$ complexity. If $N=10,000$, $N^2 = 100,000,000$ operations. That transforms a 10KB payload processing time from milliseconds to seconds (or minutes), freezing the Goroutine responsible for the request.
The Code: Limits? We Don't Need Limits.
The fix provided in CL 709876 is a textbook example of "defensive coding 101: always have an exit strategy." The original code would happily iterate through lists of active formatting elements without a hard stop, trusting that the stack wouldn't be maliciously deep. The patch introduces a constant overhead limit.
Here is a conceptual look at the vulnerable logic flow versus the patched logic:
// Vulnerable logic (simplified)
for {
// Walk up the stack looking for specific elements
// If the stack is 50,000 items deep, this loop runs 50,000 times
entry := findFormattingElement(stack)
if entry == nil {
break
}
// complex tree manipulation logic...
}The fix introduces a sanity check. It acknowledges that valid HTML shouldn't require thousands of stack adoption steps.
// Patched logic (simplified)
const maxIterations = 1000 // A sane limit
for i := 0; i < maxIterations; i++ {
entry := findFormattingElement(stack)
if entry == nil {
break
}
// complex tree manipulation logic...
}
if loopHitLimit {
// Stop trying to be smart. Just return or handle as error.
return
}By capping the loop, the complexity drops back towards linear $O(n)$. The parser might produce a slightly "wrong" tree for an insanely complex malicious input, but crucially, it finishes parsing.
The Exploit: Building the Stairway to Hang
Exploiting this doesn't require binary ninja skills; it requires a Python script and a bit of creativity. The goal is to maximize the work done by html.Parse per byte of input.
The Attack Scenario
Target: A Markdown-to-HTML converter or a link preview service (e.g., Slack/Discord style previews) running on a vulnerable Go backend.
The Payload
The most effective payloads typically involve abusing <table>, <select>, or <frameset> tags because they trigger specific complex modes in the parser state machine.
# PoC Generator for CVE-2025-47911
# Generates a payload that abuses the table adoption agency logic
payload = "<table>"
# Create a deep stack of formatting elements
for i in range(5000):
payload += "<b><i>"
# Trigger the parser to walk the stack repeatedly
for i in range(5000):
payload += "<p>Trigger Reconstruct</p>"
print(payload)When x/net/html parses this, every <p> tag inside that unclosed <table> forces the parser to reconcile the stack of 10,000 <b><i> tags.
> [!NOTE]
> Real-world impact depends on the server's timeout settings. However, since html.Parse is often called before any application-level timeouts (like context.WithTimeout) can interrupt the CPU-bound work, the Go runtime might not be able to schedule the cancellation effectively until a GC pause or function return.
The Impact: Why You Should Care
You might look at the CVSS score of 5.3 and think, "Medium severity? I'll patch it next month." That would be a mistake. CVSS scores for DoS often fail to capture the operational reality of modern architecture.
In a Kubernetes environment, a single pod often handles concurrent requests. If you are running a single-threaded Node.js app, one request kills the server. Go handles concurrency better via Goroutines, but this bug is CPU-bound. If an attacker sends 4-8 concurrent requests (matching your CPU core count), they can peg the CPU at 100%.
The Domino Effect:
- Liveness Probes Fail: Kubernetes sees the CPU spike and the lack of response. It kills the pod.
- Respawn Storm: The pod restarts. The attacker sends the payload again immediately.
- Cascading Failure: If the parser is part of a queue consumer (e.g., processing emails), the malicious payload might remain in the queue. Every time a worker picks it up, it dies. You now have a poisoned queue blocking all valid traffic.
Affected systems include critical infrastructure pieces like Grafana, Helm, and Podman. If you use these tools to render user-supplied content, you are vulnerable.
The Fix: Just Update It
The mitigation here is boring, which is good. You don't need to rewrite your architecture; you just need to bump a dependency.
Remediation Steps:
- Identify vulnerable dependencies:
govulncheck ./... - Update the package:
go get golang.org/x/net@v0.45.0 go mod tidy - Rebuild and redeploy.
If you cannot patch immediately (e.g., frozen codebase), you must implement strict input size limits before calling the parser. Truncating HTML input to a reasonable size (e.g., 50KB) can mitigate the worst of the $O(n^2)$ effects, turning a 10-second hang into a 100ms hiccup. However, truncation can break valid HTML, so patching is the only clean solution.
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
golang.org/x/net Golang | < v0.45.0 | v0.45.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Uncontrolled Resource Consumption) |
| Attack Vector | Network |
| CVSS v3.1 | 5.3 (Medium) |
| Impact | Denial of Service (CPU Exhaustion) |
| Patch | golang.org/x/net v0.45.0 |
| Exploit Status | PoC Available (Trivial to construct) |