CVE-2026-15308 is a high-severity denial-of-service vulnerability in CPython’s standard-library html.parser.HTMLParser. An application can consume disproportionate CPU time when it repeatedly feeds small pieces of attacker-controlled data into the parser while an HTML construct remains incomplete.
The failure is algorithmic rather than memory-corruption based. It does not give an attacker code execution, file access, authentication bypass, or direct data disclosure. The corrected Python Software Foundation assessment assigns a CVSS 4.0 score of 8.7 and identifies high availability impact without confidentiality or integrity impact. NVD separately assigns a CVSS 3.1 score of 7.5. (एनवीडी)
That distinction matters. CVE-2026-15308 should not be described as a Python takeover vulnerability. It should be treated as a potentially serious availability flaw wherever a network-facing or multi-tenant service incrementally parses untrusted HTML without firm limits on input size, chunk count, execution time, concurrency, or CPU consumption.
The most exposed systems are not necessarily those processing the largest HTML documents. A relatively modest amount of input can become expensive when it is divided across many feed() calls and keeps the parser waiting for the end of an unfinished tag, comment, declaration, processing instruction, or raw-text construct. The number and timing of parser invocations are part of the attack surface.
CVE-2026-15308 Quick Facts
| क्षेत्र | Current assessment |
|---|---|
| संवेदनशीलता | CVE-2026-15308 |
| प्रभावित घटक | CPython html.parser.HTMLParser |
| Weakness | CWE-400, Uncontrolled Resource Consumption |
| Primary impact | CPU exhaustion and service degradation |
| PSF CVSS 4.0 | 8.7 High |
| NVD CVSS 3.1 | 7.5 High |
| Network reachable | Potentially, when an application exposes the parser to remote input |
| Privileges required | None under the published CVSS vectors |
| User interaction | None |
| Confidentiality impact | None claimed in the corrected PSF vector |
| Integrity impact | None claimed in the corrected PSF vector |
| Availability impact | उच्च |
| Official affected range | CPython versions below 3.15.0 in the current CVE record |
| मूल कारण | Repeated concatenation and rescanning of a growing unparsed buffer |
| Primary remediation | Deploy a build containing the upstream fix or a verified vendor backport |
| Public exploitation status at disclosure | CISA’s SSVC entry recorded exploitation as none on July 9, 2026 |
| Safe validation approach | Local isolated timing test with strict CPU, data, and process limits |
The Python security announcement describes the issue as CPU denial of service caused by repeated unterminated markup declarations while processing uncontrolled data. NVD maps it to CWE-400 and lists CPython versions up to, but excluding, 3.15.0 in its initial configuration analysis. (Python Mail)
A version match is only the beginning of the assessment. A vulnerable Python runtime does not prove that a remote attacker can reach HTMLParser, control its input, cause many incremental calls, or consume enough shared compute to affect service. Conversely, an application may be exposed even when its direct dependency manifest contains no HTML parser package because html.parser ships inside CPython rather than as a separately installed PyPI dependency.
Disclosure Timeline
The technical issue and its repair moved quickly:
| Date | Event | कार्यात्मक अर्थ |
|---|---|---|
| July 4, 2026 | CPython issue 153030 was opened | Maintainers documented another worst-case quadratic path in HTMLParser |
| July 4, 2026 | The main corrective pull request was merged | The parser’s incremental buffering strategy changed |
| July 4, 2026 onward | Backports were prepared for maintained Python branches | Source branches could receive the fix before new release artifacts appeared |
| July 9, 2026 | The Python Software Foundation published its security announcement | The issue became a public high-severity CPython vulnerability |
| July 9, 2026 | CISA added SSVC metadata | Exploitation was recorded as none, automatable as yes, and technical impact as partial |
| July 9, 2026 | PSF corrected the CVE’s affected range and severity data | The range changed from below 3.16.0 to below 3.15.0, and the impact was narrowed to availability |
| July 10, 2026 | NVD added its initial analysis | NVD assigned CVSS 3.1 7.5 and listed relevant patch references |
The CPython issue was opened on July 4 and carried security and maintenance-branch labels. The associated pull request explains that both rescanning and concatenation became quadratic when an unterminated construct spanned many feed() calls. (गिटहब)
The public Python security announcement followed on July 9. It classified the flaw as high severity and directed users to the CVE record and upstream patch for current version information. (Python Mail)
Why Early Severity Reports Disagreed
The CVE change history shows that the first CVSS 4.0 data submitted for the record incorrectly marked high impact across confidentiality, integrity, and availability for both vulnerable and subsequent systems. The Python Software Foundation later corrected the vector to:
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
That corrected vector produces an 8.7 High score and describes an availability-only vulnerability. NVD’s separate CVSS 3.1 assessment is:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
That produces a 7.5 High score. (एनवीडी)
Some secondary databases may continue to show the earlier 10.0 value until their records refresh. That stale figure should not be used to claim that the flaw allows full compromise. For current triage, the corrected PSF vector, the NVD change history, the official security announcement, and the actual patch behavior are stronger evidence than an uncorrected snapshot.
The correction does not make the issue trivial. A remotely reachable CPU amplification flaw can still exhaust web workers, saturate a shared node, trigger expensive autoscaling, delay background processing, and deny service to other tenants. Severity should be tied to the application architecture and exposure, not inflated into unsupported claims about code execution.
How HTMLParser Normally Processes Incremental Data
html.parser.HTMLParser is a standard-library base class for processing HTML and XHTML text. It accepts imperfect markup, recognizes tags, comments, data, declarations, and other constructs, and calls handler methods implemented by a subclass. It is intentionally more tolerant than a strict XML parser and does not enforce every structural relationship between opening and closing tags. (पाइथन दस्तावेज़ीकरण)
A basic application can submit a complete document in one call:
from html.parser import HTMLParser
class TextCollector(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.parts: list[str] = []
def handle_data(self, data: str) -> None:
self.parts.append(data)
parser = TextCollector()
parser.feed("<p>Hello <strong>world</strong></p>")
parser.close()
print("".join(parser.parts))
The same parser also supports streaming input:
parser = TextCollector()
for chunk in [
"<p>Hello ",
"<strong>",
"world",
"</strong>",
"</p>",
]:
parser.feed(chunk)
parser.close()
The official documentation states that feed() processes complete elements and buffers incomplete data until more data arrives or close() is called. That buffering behavior is what allows a tag, attribute, comment, or other structure to span multiple chunks. (पाइथन दस्तावेज़ीकरण)
Incremental parsing is useful when data comes from:
- A socket or streaming HTTP client
- An asynchronous body iterator
- A file read in fixed-size blocks
- A compressed stream
- An email or message-processing pipeline
- A web crawler
- A multipart upload
- A queue that delivers fragments
- A transformation pipeline with bounded internal buffers
The security boundary changes when the sender controls both the content and the fragmentation pattern. A parser that sees an unfinished construct must retain enough state to decide whether later characters complete it. If every tiny addition causes the entire retained buffer to be copied and rescanned, the cost of processing can grow much faster than the amount of input.
Normal Progress and Stalled Progress
| Input pattern | Parser behavior | Typical cost profile |
|---|---|---|
| Complete tags arrive regularly | Parsed data is consumed from the buffer | Work generally tracks input size |
| A structure spans a few moderate chunks | Small amount of temporary buffering | Usually unremarkable |
| One large incomplete structure arrives once | One large scan may occur | Potentially expensive but not necessarily amplified by call count |
| An incomplete structure receives thousands of tiny additions | The same growing prefix may be examined repeatedly | Vulnerable implementations can approach quadratic work |
| The parser is closed with pending data | Remaining buffered material is handled as end of input | Cost depends on retained state and implementation |
The difference between the fourth row and the others is central to CVE-2026-15308. The problem is not simply “malformed HTML.” It is malformed or incomplete state combined with repeated incremental processing.
The Quadratic Cost in Plain Terms

Assume an application starts feeding an unfinished comment and then submits n chunks of c characters. The parser cannot finish the comment because its terminating marker has not arrived.
On a vulnerable implementation, the work can look conceptually like this:
feed 1 scans about c bytes
feed 2 scans about 2c bytes
feed 3 scans about 3c bytes
...
feed n scans about nc bytes
The accumulated scanning work is approximately:
c + 2c + 3c + ... + nc
That sum is:
c × n × (n + 1) / 2
The dominant term is proportional to n².
There was also a copying problem. Python strings are immutable. An operation conceptually equivalent to:
rawdata = rawdata + new_chunk
must create a new string containing the old buffer and the new data. When the old string becomes larger on every call, repeated concatenation can create another quadratic series of copies.
The upstream pull request explicitly identifies both costs: rescanning the growing buffer and concatenating each new piece were quadratic when an unterminated tag or comment extended across many feed() calls. (गिटहब)
Why Fragmentation Changes the Cost
Consider two applications receiving the same 256 KiB of input.
Application A makes one parser call:
parser.feed(full_body)
Application B receives 8,192 chunks of 32 bytes:
for chunk in body_chunks:
parser.feed(chunk)
The byte count is the same. The number of parser state transitions is not.
If the data contains a construct that remains unresolved until the end, Application B may repeatedly ask the vulnerable parser to reconsider almost everything it has already seen. Application A ordinarily performs far fewer complete-buffer passes because it presents the data at once.
This is why a reverse proxy’s maximum body-size setting is useful but incomplete. It limits total bytes, yet it does not necessarily limit the number of chunks, parser calls, concurrent parsing jobs, or CPU seconds spent per byte.
The Attacker’s Advantage
An algorithmic denial-of-service flaw becomes dangerous when the attacker spends substantially fewer resources than the server. The attacker may transmit a modest amount of text at ordinary network speed while the server performs many times more local work.
The amplification may be especially damaging when:
- Each request occupies a synchronous worker
- Multiple parsing requests run concurrently
- Workers share a small CPU quota
- Requests are retried automatically
- A queue repeatedly redelivers timed-out jobs
- Autoscaling adds instances without stopping the input source
- Parsing happens before authentication or rate enforcement
- One tenant can consume resources shared with others
MITRE classifies CVE-2026-15308 as CWE-400, Uncontrolled Resource Consumption. CWE-400 guidance emphasizes scale limits, throttling, and safe failure behavior, while also warning that such controls may only raise the attacker’s cost rather than remove the underlying weakness. (सामूहिक रूप से)
Which Incomplete Constructs Matter
The short CVE description refers to repeated unterminated markup declarations. The upstream patch and regression tests address the broader parser behavior associated with incomplete constructs spanning many small feeds.
The official test covers representative states such as:
- An unfinished HTML comment beginning with
<!-- - A processing instruction beginning with
<? - A declaration beginning with
<!doctype - A CDATA section
- An incomplete start tag
- A start tag with an unfinished attribute
- A raw-text
scriptसंदर्भ
The upstream regression test deliberately submits a large number of fixed-size chunks to each state and confirms that the corrected parser completes promptly. (गिटहब)
These examples should be interpreted as parser states, not as a ready-made remote exploitation checklist. Real reachability depends on how an application transports data into feed(). A WSGI application that reads and concatenates the entire request before parsing behaves differently from an asynchronous service that forwards each network chunk directly into the parser.
The broad lesson is that any parsing state that cannot make progress may become expensive if the implementation reprocesses all retained input on every addition.
How the Patch Removes Repeated Work
Before the fix, feed() effectively appended the new string directly to rawdata and invoked the parser immediately:
self.rawdata = self.rawdata + data
self.goahead(0)
The corrected implementation introduces three internal state values:
self._pending = []
self._pending_len = 0
self._parse_threshold = 1
New data is accumulated rather than always copied into the main buffer. The parser only joins and processes pending chunks when enough data has accumulated. If parsing consumes part of the buffer, the threshold is reset so normal incremental progress remains responsive. If parsing consumes nothing, the threshold grows to the current buffer length, effectively waiting for the buffered data to approximately double before another full attempt. (गिटहब)
The strategy is important because it changes how often an unresolved buffer is revisited.
A simplified conceptual version looks like this:
def feed(data):
pending_length += len(data)
if pending_length < parse_threshold:
pending.append(data)
return
pending.append(data)
rawdata += "".join(pending)
pending.clear()
pending_length = 0
old_length = len(rawdata)
parse_available_data()
if len(rawdata) < old_length:
parse_threshold = 1
else:
parse_threshold = len(rawdata)
This is not a drop-in replacement for CPython’s code. It illustrates the scheduling decision.
When the parser is making progress, it continues processing promptly. When it is stuck, it avoids repeating a full scan for every tiny addition. Rechecking after geometric growth bounds the number of times earlier bytes are reconsidered.
Old and New Behavior
| Condition | Vulnerable behavior | Corrected behavior |
|---|---|---|
| New data arrives | Concatenate immediately | Add to a pending list |
| Parser can consume data | Parse and continue | Parse and reset threshold |
| Parser cannot consume data | Retry on the next feed regardless of size | Wait for substantially more data |
| Buffer growth | Repeated immutable-string copying | Batched joining |
| Unresolved input across many calls | Near-quadratic cumulative work | Far fewer full-buffer scans |
close() with pending chunks | Process raw buffer | Join pending chunks, then process end of input |
This design addresses both sources of amplification: it avoids repeated copying of the entire accumulated string and avoids rescanning that string after every small chunk.
It does not make unbounded HTML processing safe in every respect. A patched parser can still receive very large input, run expensive application handlers, create excessive output, or participate in a larger resource-exhaustion chain. Patching removes this specific algorithmic failure; input budgets and isolation remain necessary.
Affected Versions and Patch Reality
The current CVE data marks CPython versions below 3.15.0 as affected. The record was initially created with a range below 3.16.0, then corrected by the Python Software Foundation to below 3.15.0. NVD reflects the corrected range in its change history and CPE analysis. (एनवीडी)
That range must be interpreted carefully.
First, a version range in a CVE record describes upstream source history. It does not account perfectly for every Linux distribution, cloud runtime, container rebuild, or vendor backport.
Second, “below 3.15.0” includes pre-release builds such as alphas, betas, and release candidates that were created before the fix. A pre-release bearing the 3.15 series name should not be treated as fixed unless its build date or source provenance confirms inclusion of the patch.
Third, a fix merged into a maintenance branch does not instantly appear in the latest downloadable maintenance release. For example, Python 3.14.6 and Python 3.13.14 were released on June 10, 2026, while the corrective pull request was merged on July 4. Original upstream binaries from those releases therefore predate the patch. (Python.org)
Fourth, an operating-system vendor can apply the patch to a package while preserving a version string such as 3.13.14. In that case, a scanner that compares only the interpreter version may continue reporting the package as vulnerable even though the vendor build contains the fix.
What the Upstream Evidence Shows
The CPython issue and pull request track backports for the supported 3.10 through 3.15 lines. NVD references several concrete commits, including the primary fix and branch-specific patches. (एनवीडी)
Useful patch identifiers include:
bcf98ddbc40ec9b3ee87da0124a5660b19b7e606
e9f92ac0b298292e7ff998e52cb8ccacfb27a0bd
07efb08123ba9367a7107325adb9d5626dca1ca9
7933f4bf7131aa4140750f9404f5de0aa2969ced
Do not require every deployed build to expose one of those exact Git hashes. Distribution packages often reconstruct or modify patches. Treat the identifiers as evidence for source review and vendor comparison, not as the only acceptable proof.
Evidence Strength for Patch Verification
| प्रमाण | What it proves | Confidence |
|---|---|---|
python --version is outside the affected range | The reported upstream version may contain the fix | Moderate to high, subject to pre-release and packaging details |
python --version is inside the affected range | The build might be vulnerable | Low as a final conclusion |
| Vendor advisory names CVE-2026-15308 and the installed package | The vendor claims its package contains remediation | उच्च |
| Package changelog references the CVE or upstream issue | The package likely includes a backport | उच्च |
| Source or build provenance contains the corrected logic | The relevant implementation is present | Very high |
| Safe behavioral regression test completes within expected limits | The tested runtime does not show the old scaling under that test | High for that runtime and test path |
| Production service survives an uncontrolled stress attempt | Nothing reliable and potentially creates an incident | Invalid validation method |
The strongest operational conclusion usually combines package provenance and a bounded behavioral test. Either signal alone can be misleading: changelogs can describe a package that is not actually deployed, while timing tests can be noisy or fail to cover a customized runtime.
The Conditions Required for Remote Impact
A remotely exploitable application path generally needs all of the following:
- The application runs an affected CPython implementation.
- Code imports or indirectly invokes
html.parser.HTMLParser. - Input is controlled by an unauthenticated or low-trust party.
- The application sends input to
feed()in many pieces. - The supplied markup keeps the parser in an unresolved state.
- Input size, chunk count, processing time, rate, or concurrency is insufficiently bounded.
- The parsing worker shares resources with a service whose availability matters.
Removing any one condition can reduce or eliminate direct remote reachability.
For example, an internal script that parses a fixed, signed HTML report once per day may run a vulnerable interpreter but have no plausible attacker-controlled path. A public Markdown preview endpoint may be far more exposed if its Markdown engine passes embedded raw HTML through HTMLParser as the request arrives.
Reachability Questions
Security teams should ask:
- Where is
HTMLParserinstantiated? - Is it subclassed directly?
- Does a third-party library instantiate it?
- Can a remote user influence the parsed data?
- Is authentication required before parsing begins?
- Does the caller invoke
feed()once or in a loop? - What determines chunk size?
- Can a client influence transfer fragmentation?
- Is compressed input expanded before parsing?
- What maximum uncompressed size is accepted?
- Is HTML parsing performed in the request worker?
- Is there a hard execution deadline?
- Can a stuck task be terminated, or only abandoned?
- How many jobs can run concurrently?
- Does retry logic resubmit failed content?
- Are CPU limits applied to the parser container?
- Does autoscaling react to the resulting load?
- Can one tenant affect another tenant’s capacity?
These questions are more valuable than asking only whether “Python is installed.”
High-Risk Application Patterns
HTML Preview and Sanitization Services
A service may accept user-authored HTML and generate a preview, plain-text representation, safety report, or normalized document. Even when the service later sanitizes output, it may first pass the raw input through HTMLParser.
The vulnerable operation occurs before sanitization has a chance to remove dangerous structures. “We sanitize the HTML” is therefore not sufficient evidence that the parser cannot be reached with incomplete input.
Risk rises when the preview is generated before authentication, on every keystroke, or through a synchronous endpoint. One client may initiate many overlapping previews, each occupying a worker.
Markdown Rendering
Markdown engines often permit embedded HTML or use HTML parsing during preprocessing. The direct application code may never import HTMLParser; the call can occur inside a Markdown extension or rendering library.
The relevant questions are:
- Whether raw HTML is enabled
- Which parser processes it
- Whether exceptions and time budgets are enforced
- Whether rendering occurs synchronously
- Whether preview requests are rate limited
- Whether the result is cached by content hash
Disabling raw HTML can be a useful temporary measure when business requirements allow it, but only code and dependency tracing can confirm that every HTMLParser path is removed.
Email Processing
Email security gateways, support systems, marketing platforms, and ticketing services frequently parse HTML email bodies. Input may come from any external sender and may be processed before spam classification, tenant routing, or user-level policy.
Email systems are particularly susceptible to retry amplification. If a parsing task times out without being acknowledged, a queue or mail transport may deliver it again. The same malformed content can repeatedly consume CPU unless the system records a deterministic failure state or quarantines the message.
Web Crawlers and Content Extraction
Crawlers consume attacker-controlled pages by design. A malicious site can serve content crafted to stress a parser, vary it per request, or return it only to selected user agents.
Crawlers also tend to run at concurrency. A single hostile domain may provide many URLs that all enter the same parsing pool. Per-host concurrency, body-size limits, decompression limits, parse deadlines, and domain-level circuit breakers are therefore important even after patching.
RSS and Feed Readers
RSS and Atom documents can contain embedded HTML fragments. A service may parse the XML correctly, then pass description or content fields to an HTML parser.
The upstream feed itself may be controlled by an external publisher, while a user merely subscribes to the URL. This creates an indirect remote path: the attacker does not contact the parsing endpoint directly but influences content that the service later retrieves.
HTML to Text and Search Indexing
Indexing pipelines commonly convert HTML into text before tokenization, classification, or storage. The parser may run in a background worker rather than a web server, but resource exhaustion can still delay an entire ingestion queue.
In multi-tenant systems, a single customer uploading many documents can monopolize shared indexing capacity. Queue-level fair scheduling and per-tenant budgets matter as much as HTTP rate limits.
AI and RAG Ingestion Pipelines
Retrieval systems often fetch web pages, strip markup, split text, generate embeddings, and store results. HTML parsing happens early, before expensive downstream operations.
A CPU denial of service in the parser can:
- Stall a crawl
- Hold a worker slot
- Delay document freshness
- Trigger duplicate fetches
- Expand queue lag
- Cause downstream retries
- Inflate compute costs
- Prevent other tenants from ingesting data
The presence of a model does not change the parser’s trust boundary. Retrieved web content remains untrusted data. Parser isolation, URL-level budgets, content-type checks, decompression limits, and deterministic failure handling are required.
CI Documentation Builds
Documentation platforms may render pull-request descriptions, Markdown pages, API documentation, or generated HTML. An external contributor can sometimes influence the source without having deployment access.
Although CI capacity is usually separate from production web capacity, a parser DoS can block builds, delay releases, exhaust shared runners, or consume billed minutes. Repository trust policy should determine whether untrusted pull requests receive full build resources.
Safe Local PoC for Defensive Validation
The following demonstration is intentionally limited.
It:
- Runs only on the local machine
- Makes no network requests
- Does not scan any target
- Uses a fixed, modest amount of data
- Runs each measurement in a separate child process
- Terminates the child after two seconds
- Stops increasing work if a trial times out
- Measures scaling rather than attempting to make the host unavailable
Do not point this logic at a production endpoint. Do not remove the timeout or increase the counts on a shared system. The purpose is to compare a locally approved runtime before and after remediation.
#!/usr/bin/env python3
"""
Bounded local behavior check for CVE-2026-15308.
Safety properties:
- No networking
- At most 256 KiB of repeated chunk data in the largest trial
- One child process per trial
- Two-second hard timeout per child
- Automatic termination on timeout
This is a regression-oriented timing check, not an exploit scanner.
"""
from __future__ import annotations
from html.parser import HTMLParser
from multiprocessing import Process, Queue
from queue import Empty
from time import perf_counter
from typing import Final
PREFIX: Final[str] = "<!--"
CHUNK: Final[str] = "a" * 32
SUFFIX: Final[str] = "-->"
FEED_COUNTS: Final[tuple[int, ...]] = (
1_000,
2_000,
4_000,
8_000,
)
TRIAL_TIMEOUT_SECONDS: Final[float] = 2.0
def run_trial(feed_count: int, result_queue: Queue) -> None:
"""Execute one bounded local parser trial."""
parser = HTMLParser()
start = perf_counter()
parser.feed(PREFIX)
for _ in range(feed_count):
parser.feed(CHUNK)
parser.feed(SUFFIX)
parser.close()
elapsed = perf_counter() - start
result_queue.put(elapsed)
def measure(feed_count: int) -> float | None:
"""Return elapsed seconds, or None when the hard timeout fires."""
result_queue: Queue = Queue(maxsize=1)
process = Process(
target=run_trial,
args=(feed_count, result_queue),
daemon=True,
)
process.start()
process.join(TRIAL_TIMEOUT_SECONDS)
if process.is_alive():
process.terminate()
process.join()
return None
try:
return float(result_queue.get_nowait())
except Empty:
raise RuntimeError(
f"Trial {feed_count} exited without returning a result"
)
def main() -> None:
previous: float | None = None
print("feeds\tbytes\tseconds\tratio")
print("-" * 48)
for feed_count in FEED_COUNTS:
elapsed = measure(feed_count)
total_chunk_bytes = feed_count * len(CHUNK)
if elapsed is None:
print(
f"{feed_count}\t{total_chunk_bytes}\tTIMEOUT\t-"
)
print("Stopped after the bounded child-process timeout.")
break
ratio = "-"
if previous is not None and previous > 0:
ratio = f"{elapsed / previous:.2f}x"
print(
f"{feed_count}\t{total_chunk_bytes}\t"
f"{elapsed:.6f}\t{ratio}"
)
previous = elapsed
if __name__ == "__main__":
main()
Save the file as safe_htmlparser_probe.py and run it in an isolated development environment:
python3 safe_htmlparser_probe.py
For stronger containment, run an approved test image with explicit Docker limits:
docker run --rm \
--cpus="0.50" \
--memory="256m" \
--pids-limit="64" \
-v "$PWD:/lab:ro" \
-w /lab \
your-approved-python-image \
python safe_htmlparser_probe.py
Docker containers have no resource constraints by default and can consume resources permitted by the host scheduler. Explicit CPU and memory flags therefore matter when testing a resource-exhaustion condition. (Docker Documentation)
Interpreting the Output
The script reports the time taken as the number of feed() calls doubles.
A vulnerable runtime may show ratios that trend upward toward roughly four times the previous duration, because doubling the input count can create approximately four times the quadratic work. A corrected runtime should not maintain that pattern.
Do not expect mathematically perfect ratios. Results can be distorted by:
- Process startup cost
- CPU frequency scaling
- Other workloads
- Virtualization
- Container throttling
- Python build options
- Very small sample sizes
- Timer resolution
- Operating-system scheduling
A single noisy run is not proof of vulnerability or remediation. Repeat the test a small number of times in a dedicated lab, compare medians, and combine the result with package provenance.
The official CPython regression test is much larger than this demonstration and feeds 200,000 chunks of 64 characters across several incomplete parser states. That scale is appropriate for upstream performance testing under controlled conditions, not for an ordinary production host. (गिटहब)
What the PoC Teaches
The demonstration helps defenders understand four properties:
- The input does not need to execute code.
- An unfinished parser state is enough to retain data.
- Repeated small calls can matter more than raw document size.
- A hard process boundary prevents the test from running indefinitely.
It does not establish that a particular web application is remotely exploitable. Application-level validation requires proof that remote input reaches the same call pattern, but that proof should be developed through code tracing and controlled staging tests rather than a production stress attempt.
Finding HTMLParser in a Codebase
Because html.parser is part of CPython, a package vulnerability scanner may identify the runtime but will not show a separate dependency such as html-parser==x.y.
Start with source discovery:
rg -n \
'from[[:space:]]+html\.parser[[:space:]]+import|import[[:space:]]+html\.parser|HTMLParser[[:space:]]*\(' \
.
Find subclasses:
rg -n \
'class[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\([^)]*HTMLParser[^)]*\)' \
.
Find calls to feed():
rg -n '\.feed[[:space:]]*\(' .
The last search will produce false positives because many libraries use a method named feed. Review results in files that import HTMLParser or receive a parser object.
Search for streaming loops near parser calls:
rg -n \
'iter_bytes|iter_text|aiter_bytes|aiter_text|read\(|recv\(|stream|chunk|body_iterator' \
.
Look for likely indirect consumers:
rg -n \
'markdown|html2text|BeautifulSoup|email\.message|feedparser|sanitize|crawler|scrape|render_html' \
pyproject.toml requirements*.txt poetry.lock uv.lock setup.cfg setup.py
The presence of another parser does not prove it uses HTMLParser. For example, a library may use lxml, html5lib, a C extension, or an internal tokenizer. Trace the installed code or inspect runtime stack samples before assigning the CVE.
A Practical Reachability Review
For each match, record:
| Question | Evidence to collect |
|---|---|
| Where is the parser created? | File, function, class, and owning service |
| Who controls the input? | Public user, authenticated tenant, employee, trusted file, or fixed asset |
| How is data delivered? | One string, fixed-size chunks, async iterator, file reads, or queue fragments |
| Can input remain incomplete? | Parser state and caller behavior |
| What is the maximum size? | Application and proxy configuration |
| What is the maximum chunk count? | Caller logic or stream constraints |
| What deadline applies? | Worker, job, and process timeout |
| Can the computation be killed? | Process isolation or only request cancellation |
| What resources are shared? | Worker pool, node, tenant, queue, or database |
| What retries occur? | HTTP client, task queue, scheduler, or message broker |
| Which build is deployed? | Package version, image digest, vendor advisory, and patch evidence |
The review should produce one of several explicit states:
- Not present — The runtime or component is not used.
- Present but unreachable — The parser exists but receives only controlled input.
- Candidate exposure — Untrusted input may reach the parser.
- Behavior safely reproduced — A bounded staging or local test confirmed vulnerable scaling.
- Patched by provenance — Vendor or build evidence confirms the fix.
- Remediation retested — A controlled test confirms that the old behavior no longer appears.
- Compensating controls only — Risk is reduced, but the vulnerable implementation remains.
This vocabulary prevents a scanner finding from being mistaken for proof.
Inventorying the Actual Python Runtime
The application’s shell environment may not match the interpreter used by its workers. Collect evidence inside the running container, virtual machine, function runtime, or service process.
Display detailed version information:
python3 -VV
Display the executable, version, and module path:
python3 - <<'PY'
import html.parser
import sys
print("executable:", sys.executable)
print("version:", sys.version)
print("version_info:", sys.version_info)
print("html.parser:", html.parser.__file__)
PY
Check whether multiple interpreters exist:
command -v -a python python3 2>/dev/null || true
find /usr /opt /app -type f \
\( -name 'python3' -o -name 'python3.*' \) \
-perm -111 2>/dev/null | sort
For containers, record the image digest rather than only the mutable tag:
docker image inspect your-image:tag \
--format '{{json .RepoDigests}}'
For Kubernetes:
kubectl get pod YOUR_POD \
-o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.imageID}{"\n"}{end}'
A deployment may contain different runtimes in:
- The web process
- A Celery or RQ worker
- A crawler
- A document converter
- A scheduled job
- An administrative utility
- A serverless function
- A build image
- A migration container
- A development notebook
Every path that processes untrusted HTML needs independent review.
Verifying Distribution Backports
Operating-system vendors commonly patch security issues without changing the upstream interpreter’s visible feature version. The exact package command varies by distribution.
On Debian or Ubuntu systems, begin with:
dpkg-query -W \
-f='${Package}\t${Version}\n' \
'python3*' 2>/dev/null | sort
Review available package metadata:
apt-cache policy python3 python3-minimal
Search installed changelogs when present:
zgrep -i \
'CVE-2026-15308\|gh-153030\|quadratic complexity.*HTMLParser' \
/usr/share/doc/python3*/changelog* 2>/dev/null
On RPM-based systems:
rpm -qa | grep -E '^python3' | sort
Search package changelogs:
rpm -q --changelog python3 2>/dev/null \
| grep -i -E \
'CVE-2026-15308|gh-153030|HTMLParser'
On Alpine:
apk info -vv python3
apk info -a python3
These commands are discovery aids. Package names differ across releases, and a missing CVE string does not prove that the patch is absent. Vendors may reference an upstream issue, commit, bug tracker, or umbrella security update instead.
The defensible sequence is:
- Identify the exact installed package.
- Find the vendor’s advisory or package change record.
- Confirm that the deployed repository and architecture match the advisory.
- Restart or redeploy all processes using the old interpreter.
- Record the resulting image digest or package build.
- Run a bounded regression check in a representative environment.
- Preserve the evidence with the remediation ticket.
Why Version Matching Alone Fails
A scanner may report every Python 3.14.6 environment as vulnerable because the upstream release predates the patch. That is a useful candidate signal.
It may still be wrong in either direction:
- A Linux vendor may have backported the fix into its
3.14.6package. - A custom image may report a later-looking label while still containing an older interpreter layer.
- A virtual environment may point to a different system interpreter than expected.
- A running pod may not have restarted after the image was rebuilt.
- A patched source branch may have been documented before a binary artifact was published.
- An application may contain the affected runtime but never use the parser with uncontrolled data.
- A service may use a vulnerable parser indirectly even though a simple source search misses it.
Teams using automated or AI-assisted validation should keep version observation, reachability, safe reproduction, and remediation confirmation as separate evidence states. Penligent’s discussion of why CVE validation needs more than matching version strings makes the same operational distinction: a version can establish candidacy, but it does not prove reachable vulnerable behavior.
When an AI-assisted testing platform such as पेनलिजेंट is used in an authorized workflow, the safety policy should require an isolated target, bounded test data, enforced rate and resource limits, preserved raw evidence, and a clear distinction between a hypothesis and a verified finding. An autonomous system should not decide to stress a production parser merely because it recognized a CVE-compatible version.
Detection Engineering for CVE-2026-15308
No single log line proves exploitation. Detection should correlate application behavior, parser timing, request characteristics, worker health, and infrastructure resource consumption.
Application Metrics
Instrument the parsing boundary with:
- Input bytes
- Number of chunks
- Minimum, maximum, and average chunk size
- Parsing wall-clock duration
- Process CPU time where available
- Parser completion status
- Timeout or rejection reason
- Route or job type
- Tenant identifier
- Authenticated or unauthenticated state
- Worker identifier
- Content hash
- Retry count
Avoid recording complete untrusted HTML by default. It may contain personal data, credentials, proprietary text, tracking tokens, or active content. A cryptographic hash, bounded prefix, structural classification, and secure quarantined sample are often sufficient for correlation.
A defensive wrapper can emit structured logs:
import hashlib
import json
import logging
from time import perf_counter, process_time
logger = logging.getLogger("html_parser")
def log_parse_result(
*,
body: str,
chunk_count: int,
status: str,
started_wall: float,
started_cpu: float,
) -> None:
event = {
"event": "html_parse",
"status": status,
"input_characters": len(body),
"chunk_count": chunk_count,
"sha256": hashlib.sha256(
body.encode("utf-8", errors="replace")
).hexdigest(),
"wall_seconds": perf_counter() - started_wall,
"cpu_seconds": process_time() - started_cpu,
}
logger.info(json.dumps(event, separators=(",", ":")))
The most informative ratio may be CPU time per kilobyte. A request containing only a few hundred kilobytes but consuming seconds of CPU deserves attention even when its body size remains below the normal upload limit.
Infrastructure Signals
Watch for:
- One Python worker pinned near one core
- Several workers reaching their timeout together
- Increased CPU throttling in parser pods
- Queue age increasing while request volume remains normal
- Worker restart counts rising
- Readiness probes failing
- 502, 503, or 504 responses on HTML-processing routes
- Autoscaling without a corresponding rise in successful throughput
- Increased task retries
- Higher cost per document or per request
- Other tenants experiencing latency during one tenant’s ingestion burst
A CPU denial of service may look different from an ordinary traffic flood. Request count can remain modest while cost per request rises sharply.
Request Characteristics
Potentially useful indicators include:
- Very high chunk count relative to body size
- Repeated tiny chunks
- Long-lived incomplete HTML structures
- Multiple parse timeouts tied to the same content hash
- Similar inputs distributed across source addresses
- Many distinct inputs from one tenant that create the same parser state
- Repeated retries after worker termination
- Content-type mismatches, such as HTML submitted through an endpoint intended for plain text
Do not create a detection rule that assumes every unfinished comment is malicious. Real content is often truncated or malformed. The security signal is the combination of structure, repeated delivery, disproportionate CPU use, and operational effect.
A Generic Alert Model
A useful alert might require:
html_parse_status = timeout
AND input_bytes < 1 MiB
AND parser_cpu_seconds > 0.5
AND chunk_count > 1000
AND repeated_count_by_content_hash >= 2
Thresholds must be calibrated to the service. An offline crawler can tolerate more parsing time than an interactive preview endpoint. A 0.5-second CPU budget may be generous for one application and too strict for another.
Stack and Profile Evidence
During an incident, sample the hot process with an approved profiler. Useful evidence may include repeated activity in:
html.parser- Internal parser state functions
- String concatenation or copying
- Regular-expression processing used by parsing states
- Application handlers invoked by
HTMLParser
Profiling production carries risk. Prefer low-overhead sampling, capture only long enough to identify the code path, and coordinate with the service owner.
Why WAF Signatures Are Not a Complete Fix
A web application firewall can block obvious patterns, but CVE-2026-15308 is primarily a state and complexity problem.
A brittle rule might block <!-- followed by a long string without -->. That does not cover:
- Incomplete start tags
- Processing instructions
- Declarations
- CDATA-like states
- Raw-text elements
- Encoding transformations
- Fragmentation across transport buffers
- Content delivered through queues rather than HTTP
- HTML embedded in Markdown, email, XML, or JSON
- Inputs transformed before reaching the parser
Aggressive signatures can also block legitimate truncated documents, comments, templates, or source-code examples.
Use a WAF as a temporary outer layer when appropriate, but prioritize:
- A patched runtime
- Total-byte limits
- Chunk-count limits
- Minimum useful chunk size
- Authentication before expensive processing
- Per-principal rate limits
- Bounded concurrency
- Process-level deadlines
- CPU quotas
- Queue isolation
The closer a control sits to the actual parser call, the more accurately it can measure the resource being protected.
Remediation Priorities
Deploy a Verified Patch
The primary remediation is to install a CPython build that includes the upstream change or a vendor-confirmed backport.
A complete deployment includes more than updating a package repository:
- Rebuild all affected images.
- Verify that no cached layer restores the old interpreter.
- Roll out the new image digest.
- Restart long-running workers.
- Replace scheduled-job images.
- Update serverless runtimes when the provider makes a fixed build available.
- Retest the actual runtime inside a new process.
- Confirm that old pods or virtual machines have terminated.
- Update exception and vulnerability records.
- Monitor parse latency and CPU after deployment.
Do not claim remediation based only on a pull request being merged upstream.
Temporarily Disable Untrusted Incremental Parsing
When a fixed build is not immediately available, consider disabling the feature that accepts raw or embedded HTML.
Possible temporary actions include:
- Disable public HTML preview
- Disable raw HTML inside Markdown
- Convert input to escaped plain text
- Quarantine external email HTML
- Pause ingestion from untrusted URLs
- Route parsing through an isolated worker tier
- Require authentication for the feature
- Reduce per-tenant concurrency
These actions can have product impact. Document the tradeoff and restore functionality only after the patched path is verified.
Bound Total Input
Reject unexpectedly large inputs before parsing.
A byte budget should apply to the expanded representation, not only compressed transfer size. Otherwise, a small compressed body can expand into a much larger string.
उदाहरण:
MAX_INPUT_BYTES = 256 * 1024
def enforce_utf8_budget(text: str) -> None:
size = len(text.encode("utf-8", errors="replace"))
if size > MAX_INPUT_BYTES:
raise ValueError(
f"HTML input exceeds {MAX_INPUT_BYTES} bytes"
)
A byte limit reduces the maximum amount of work but does not fully address call-count amplification. A body below the limit can still be divided into many small feeds.
Bound the Number of Chunks
If the application controls the loop, reject or coalesce excessive fragmentation:
from collections.abc import Iterable
MAX_BYTES = 256 * 1024
MAX_CHUNKS = 512
MIN_COALESCED_CHARACTERS = 4096
def coalesce_bounded_html(chunks: Iterable[str]) -> str:
parts: list[str] = []
buffered: list[str] = []
total_bytes = 0
chunk_count = 0
buffered_characters = 0
for chunk in chunks:
if not isinstance(chunk, str):
raise TypeError("HTML chunks must be strings")
chunk_count += 1
if chunk_count > MAX_CHUNKS:
raise ValueError("Too many HTML chunks")
total_bytes += len(
chunk.encode("utf-8", errors="replace")
)
if total_bytes > MAX_BYTES:
raise ValueError("HTML input is too large")
buffered.append(chunk)
buffered_characters += len(chunk)
if buffered_characters >= MIN_COALESCED_CHARACTERS:
parts.append("".join(buffered))
buffered.clear()
buffered_characters = 0
if buffered:
parts.append("".join(buffered))
return "".join(parts)
This wrapper places explicit limits on bytes and chunks and avoids forwarding every transport fragment into the parser.
A temporary parse path could then call feed() once:
from html.parser import HTMLParser
from collections.abc import Iterable
def parse_bounded_chunks(
parser: HTMLParser,
chunks: Iterable[str],
) -> None:
document = coalesce_bounded_html(chunks)
parser.feed(document)
parser.close()
This is a compensating control, not an equivalent patch.
Reasons include:
- Other malformed-input complexity issues may still affect an old runtime.
- Joining the full input increases peak memory.
- Application handler methods may be expensive.
- The byte and chunk values may be inappropriate for the product.
- A large one-shot parse can still consume meaningful CPU.
- The wrapper does not protect code paths that bypass it.
Use it only while completing the runtime update or as a continuing defense in depth.
Enforce a Killable Deadline
A Python thread cannot reliably terminate another thread that is stuck in CPU-bound parsing. A request timeout may stop waiting for the result while the underlying computation continues consuming CPU.
For hard containment, place untrusted parsing in a separate process or worker that can be terminated.
A small illustrative pattern is:
from __future__ import annotations
from html.parser import HTMLParser
from multiprocessing import get_context
from multiprocessing.connection import Connection
from typing import Any
class CountingParser(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.events = 0
def handle_starttag(
self,
tag: str,
attrs: list[tuple[str, str | None]],
) -> None:
self.events += 1
def handle_data(self, data: str) -> None:
self.events += 1
def child_parse(html: str, connection: Connection) -> None:
try:
parser = CountingParser()
parser.feed(html)
parser.close()
connection.send(
{
"ok": True,
"events": parser.events,
}
)
except BaseException as exc:
connection.send(
{
"ok": False,
"error_type": type(exc).__name__,
}
)
finally:
connection.close()
def parse_with_hard_timeout(
html: str,
timeout_seconds: float = 0.5,
) -> dict[str, Any]:
context = get_context("spawn")
parent, child = context.Pipe(duplex=False)
process = context.Process(
target=child_parse,
args=(html, child),
daemon=True,
)
process.start()
child.close()
process.join(timeout_seconds)
if process.is_alive():
process.terminate()
process.join()
parent.close()
raise TimeoutError("HTML parsing exceeded its deadline")
if not parent.poll():
parent.close()
raise RuntimeError("Parser process returned no result")
result = parent.recv()
parent.close()
return result
Spawning one process per request is usually too expensive for high-volume production. A real design may use:
- A bounded worker pool
- One task at a time per worker
- Worker recycling
- OS-enforced CPU quotas
- Queue deadlines
- Cancellation that kills the worker process
- Separate pools per trust level or tenant
- A supervisor that replaces timed-out workers
The essential property is that a deadline can stop the computation, not merely stop the caller from awaiting it.
Reverse-Proxy Controls
NGINX supports maximum request-body size and body-read timeout directives. client_max_body_size rejects a body above the configured limit with a 413 response. client_body_timeout controls the allowed delay between successive body reads rather than the total duration of the entire upload. (Nginx)
A defensive starting point for a small HTML-processing endpoint might be:
http {
limit_req_zone
$binary_remote_addr
zone=html_per_ip:10m
rate=2r/s;
server {
location /api/html-preview {
client_max_body_size 256k;
client_body_timeout 10s;
limit_req
zone=html_per_ip
burst=5
nodelay;
proxy_connect_timeout 2s;
proxy_send_timeout 5s;
proxy_read_timeout 3s;
proxy_pass http://html_preview_backend;
}
}
}
The values are examples, not universal recommendations. Base them on measured legitimate traffic.
Important limitations include:
- A per-IP limit can be evaded through distributed sources.
- Many legitimate users may share one address.
- A body-read timeout does not cap parser CPU after the body arrives.
- A 256 KiB body can still be fragmented heavily.
- Internal queues and crawlers bypass the public proxy.
- The upstream worker may continue running after a client disconnects.
- An overly aggressive read timeout can block slow legitimate clients.
Apply identity-aware and tenant-aware controls inside the application where possible.
Container and Kubernetes Limits

A CPU limit reduces the blast radius when parsing runs in a container. It prevents one parser container from consuming every available core on the node, although the application inside the container may still become unavailable.
Kubernetes expresses CPU requests and limits in the container resource configuration. Its documentation states that a container cannot use more CPU than its configured limit and may be throttled when it attempts to exceed that value. (Kubernetes)
उदाहरण:
apiVersion: apps/v1
kind: Deployment
metadata:
name: html-parser
spec:
replicas: 3
selector:
matchLabels:
app: html-parser
template:
metadata:
labels:
app: html-parser
spec:
containers:
- name: parser
image: registry.example/html-parser@sha256:REPLACE_ME
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
A CPU limit is containment, not remediation.
Potential side effects include:
- Legitimate large documents taking longer
- Queue latency growing under sustained malicious input
- Readiness probes failing
- Autoscaling adding more vulnerable replicas
- The attack consuming the entire parser tier within its allowed quota
- Neighboring services remaining healthy while the parser service fails
Use resource limits together with patched code, work deadlines, bounded concurrency, and rejection of repeated pathological jobs.
Separate the Parser Tier
A strong architecture places untrusted content parsing outside the primary web worker pool:
Public API
|
| validates identity, size, type, and quota
v
Bounded Queue
|
| per-tenant fairness and deadline
v
Isolated Parser Workers
|
| CPU and memory limits
v
Validated Result Store
This design allows parser workers to fail without consuming every interactive request worker. It also provides a natural place to deduplicate content, enforce budgets, quarantine repeated failures, and retain forensic metadata.
The queue must itself be bounded. Moving unlimited malicious jobs into a queue only changes the location of the denial of service.
Rate, Concurrency, and Fairness Controls
A resource budget should be multidimensional.
| Budget | Example question |
|---|---|
| Bytes per document | How much expanded text can one task submit? |
| Chunks per document | How fragmented may the input be? |
| CPU seconds per parse | How much compute is one document allowed? |
| Wall-clock seconds | How long may a task occupy a worker? |
| Concurrent parses per tenant | Can one customer fill the pool? |
| Documents per minute | How quickly may new work arrive? |
| Failed parses per content hash | Will the same bad input be retried? |
| Queue depth | How much unfinished work can accumulate? |
| Retry count | Will timeouts multiply the load? |
| Node CPU quota | Can parser workers harm unrelated services? |
A global request limit alone can permit one tenant to monopolize all capacity. Use fair queuing or per-tenant semaphores where multi-tenant isolation matters.
Repeated deterministic failures should generally be quarantined rather than retried automatically. Record a stable content hash and parser version so the system can recognize that the same document already exceeded its budget.
Incident Response for Suspected Exploitation
CVE-2026-15308 primarily threatens availability. Incident response should begin with service stabilization and evidence preservation.
Identify the Affected Boundary
Determine:
- Which route, queue, crawler, or upload feature invoked the parser
- Which runtime and package build was executing
- Whether input was authenticated
- Which tenant or source initiated the task
- Which workers and nodes were affected
- Whether other services shared those resources
- Whether retries or autoscaling amplified the event
Do not replay the full suspicious content against production to confirm the finding.
Preserve Bounded Evidence
Collect:
- Request or task identifiers
- Timestamps
- Content length
- Chunk count if available
- Content hash
- Authentication context
- Source address or fetch URL
- Parser route
- Wall and CPU duration
- Worker PID and container ID
- Image digest
- Python build information
- Timeout and restart events
- Queue retry history
- Relevant metrics and stack samples
Store raw content only when necessary and permitted. Apply normal sensitive-data handling, access control, and retention policy.
Contain the Workload
Possible immediate actions include:
- Disable the affected feature
- Reject raw HTML
- Reduce maximum input size
- Reduce parser concurrency
- Add tenant-specific rate limits
- Quarantine repeated failing hashes
- Route work to a restricted parser pool
- Stop retries
- Lower worker deadlines
- Apply CPU limits
- Block a confirmed abusive principal
- Patch and restart the runtime
Blocking one source address may provide temporary relief but should not be the only response.
Assess Business Impact
Review:
- Failed or delayed requests
- Queue backlog
- Dropped tasks
- Worker restarts
- Autoscaling events
- Cloud cost changes
- Customer-visible latency
- SLA violations
- Other tenants affected
- Downstream timeouts
- Data that was accepted but not processed
- Jobs that may require safe replay after patching
The CVE itself does not imply that an attacker accessed secrets or modified data. Credential rotation, host reimaging, and breach notification should not be triggered solely by the presence of this parser DoS. Perform those actions only if separate evidence indicates a broader compromise.
Retest Without Recreating the Incident
After remediation:
- Confirm the new runtime provenance.
- Run the bounded local test.
- Run a small staging request through the real application path.
- Verify that time and CPU budgets still operate.
- Confirm repeated failures enter quarantine rather than retry loops.
- Observe production metrics during a controlled rollout.
- Remove temporary feature restrictions only after the evidence is complete.
Related Python Parser Vulnerabilities
CVE-2026-15308 is not the first denial-of-service issue associated with HTMLParser. Related vulnerabilities help explain why patching one historical issue does not eliminate every complexity or integration risk.
| सीवीई | अवयव | Failure mode | Distinguishing condition | Primary remediation |
|---|---|---|---|---|
| CVE-2026-15308 | CPython HTMLParser | Quadratic CPU consumption | Incomplete construct spans many incremental feed() calls | Install the current CPython fix or verified backport |
| CVE-2025-6069 | CPython HTMLParser | Worst-case quadratic processing | Certain crafted malformed inputs | Install a CPython release or vendor package containing that earlier fix |
| CVE-2025-69534 | Python-Markdown 3.8 | Unhandled AssertionError and process failure | Malformed HTML-like input reaches Python-Markdown’s HTML handling | Upgrade Python-Markdown to 3.8.1 or later fixed release |
CVE-2025-6069
CVE-2025-6069 also involved worst-case quadratic behavior in html.parser.HTMLParser when processing crafted malformed input. NVD describes the result as an amplified denial of service and maps that issue to CWE-1333. (एनवीडी)
The similarity is important but should not collapse the two issues into one.
CVE-2025-6069 concerns an earlier malformed-input complexity path. CVE-2026-15308 specifically addresses the incremental buffering behavior that remained problematic when an unfinished construct extended across many feed() calls.
A system that applied the 2025 fix can still require the 2026 fix. Security teams should not close CVE-2026-15308 merely because their records show a previous HTMLParser security update.
The mitigation for both is to maintain a currently patched CPython runtime and retain independent input and resource budgets. An old runtime can contain more than one parser weakness, so coalescing chunks should never be treated as a universal replacement for upgrading.
CVE-2025-69534
CVE-2025-69534 affected Python-Markdown 3.8. NVD reports that malformed HTML-like sequences could cause html.parser.HTMLParser to raise an AssertionError that Python-Markdown did not catch. Applications processing attacker-controlled Markdown could crash, affecting web applications, documentation systems, CI pipelines, and other rendering services. The issue was fixed in Python-Markdown 3.8.1. (एनवीडी)
This vulnerability demonstrates an adjacent risk: even when the standard-library parser is not directly exposed by application code, its behavior can propagate through a higher-level rendering package.
The remediation questions differ:
- For CVE-2026-15308, verify the CPython runtime and incremental parsing behavior.
- For CVE-2025-69534, verify the Python-Markdown package version and exception handling.
- For both, identify whether untrusted content reaches the parser and whether rendering occurs in a shared availability boundary.
Dependency and runtime inventories must therefore be reviewed together.
Common Assessment Mistakes
Calling the Vulnerability Remote Code Execution
Nothing in the corrected PSF vector, NVD analysis, public security announcement, or patch supports an RCE claim. The documented effect is CPU denial of service.
Describing it as RCE creates bad priorities and damages report credibility. Use precise language such as:
A remote unauthenticated user may be able to exhaust application CPU when attacker-controlled HTML is incrementally processed by an affected
HTMLParserpath.
Even that statement should be conditioned on verified application reachability.
Repeating the Obsolete 10.0 Score
The initial CVSS submission was corrected. Current PSF data gives CVSS 4.0 8.7, while NVD gives CVSS 3.1 7.5. The difference reflects scoring-system versions, not evidence of two separate impacts. (एनवीडी)
A report can mention the correction if stakeholders have seen the old value, but it should not continue presenting 10.0 as the authoritative current score.
Declaring Vulnerability From a Banner
A Python version inside the affected range establishes a candidate vulnerable runtime. It does not prove:
HTMLParseris used- The vulnerable call pattern exists
- Remote input reaches it
- The input is incremental
- Resource limits are absent
- A meaningful service impact is possible
Use “potentially affected” until reachability and patch status are established.
Declaring Safety From a Version String
The inverse is also dangerous.
A package may report a familiar version while carrying a vendor backport. A container tag may move. A server may still run an old process after an upgrade. A pre-release build may have a misleadingly new series name but predate the fix.
Record build provenance and test the deployed runtime.
Checking Only Application Dependencies
html.parser does not appear as a normal third-party package in आवश्यकताएँ.txt. A software-composition tool focused only on PyPI dependencies may miss the relevant runtime relationship or report it separately as an operating-system component.
Inventory:
- CPython itself
- Operating-system packages
- Container base images
- Managed runtimes
- Embedded interpreters
- Third-party libraries that call the standard parser
Treating Request Size as the Only Control
Body-size limits are useful, but the flaw is sensitive to repeated calls and unresolved state. Also limit:
- Chunk count
- Concurrent parses
- CPU time
- Wall time
- Retry count
- Queue depth
- Per-tenant capacity
Using a Thread Timeout as a Kill Switch
A future or request can time out while its underlying Python thread continues computing. A hard deadline requires a killable process, container, job, or external worker boundary.
Test the timeout mechanism itself. A configuration value named timeout does not guarantee that CPU execution stops.
Running a Stress PoC Against Production
An availability PoC can become the incident it is meant to diagnose. Production stress testing also makes results hard to interpret because rate limits, autoscaling, retries, and customer traffic interfere.
Use code tracing, package evidence, and a bounded isolated test.
Assuming Autoscaling Solves the Problem
Autoscaling can multiply cost and expand the number of vulnerable workers. If malicious requests continue arriving, more capacity may simply process more expensive inputs.
Scaling is a resilience mechanism, not a parser fix.
Building a Reliable Patch Verification Workflow
A mature verification record should answer four different questions.
Is the Component Present
साक्ष्य:
CPython executable
Full build string
Loaded html.parser path
Operating-system package
Container image digest
Runtime owner
Is the Vulnerable Path Reachable
साक्ष्य:
Application call graph
HTMLParser subclass or indirect dependency
Input trust classification
Incremental feed loop
Authentication boundary
Parser worker architecture
Is the Deployed Build Patched
साक्ष्य:
Vendor advisory
Package changelog
Upstream commit mapping
Image build record
Deployment digest
Process restart time
Does Remediation Work
साक्ष्य:
Bounded local timing result
Staging integration result
CPU and latency comparison
Timeout behavior
Retry behavior
Production rollout metrics
A finding that contains only the first category is an inventory result, not a validated vulnerability.
Defensive Regression Tests for Application Teams
The upstream CPython test protects the library implementation. Application teams should add tests for their own safety boundaries.
Test the Byte Budget
import pytest
def test_html_input_over_budget_is_rejected() -> None:
oversized = "a" * (256 * 1024 + 1)
with pytest.raises(ValueError, match="too large"):
coalesce_bounded_html([oversized])
Test the Chunk Budget
import pytest
def test_excessive_fragmentation_is_rejected() -> None:
chunks = ("a" for _ in range(513))
with pytest.raises(ValueError, match="Too many"):
coalesce_bounded_html(chunks)
Test That Normal Fragmentation Still Works
def test_normal_html_chunks_are_coalesced() -> None:
chunks = [
"<p>",
"hello ",
"<strong>",
"world",
"</strong>",
"</p>",
]
document = coalesce_bounded_html(chunks)
assert document == (
"<p>hello <strong>world</strong></p>"
)
Test the Hard Deadline
A production deadline test should execute a deliberately slow test function, not a real CVE-sized parser input:
from multiprocessing import get_context
from time import sleep
def deliberately_slow() -> None:
sleep(10)
def test_supervisor_can_terminate_worker() -> None:
context = get_context("spawn")
process = context.Process(target=deliberately_slow)
process.start()
process.join(0.1)
assert process.is_alive()
process.terminate()
process.join(1.0)
assert not process.is_alive()
This verifies that the containment mechanism works without intentionally consuming CPU.
Test Retry Quarantine
A task that exceeds its parser budget should not be submitted indefinitely:
MAX_PARSE_ATTEMPTS = 1
def should_retry_parse(
*,
failure_reason: str,
attempts: int,
) -> bool:
if failure_reason == "parser_resource_budget":
return False
return attempts < MAX_PARSE_ATTEMPTS
The exact policy depends on whether failures may be transient. A deterministic parser budget violation is usually not transient.
Prioritizing Remediation
Not every affected Python runtime carries equal operational risk.
| Priority | पर्यावरण |
|---|---|
| Critical operational priority | Public unauthenticated endpoint incrementally parsing arbitrary HTML in shared web workers |
| Very high | Multi-tenant preview, Markdown, email, crawler, or ingestion service with weak resource isolation |
| उच्च | Authenticated service where low-trust users can submit HTML repeatedly |
| मध्यम | Background parser with strict queue, CPU, and timeout controls |
| Lower | Internal service parsing controlled documents with no external input path |
| Informational until use changes | Vulnerable runtime installed but HTMLParser absent from relevant execution paths |
These categories describe remediation urgency, not CVSS replacement scores.
A high-value sequence is:
Within the First Day
- Inventory Python runtimes.
- Identify direct and indirect
HTMLParseruse. - Find public and multi-tenant parsing routes.
- Add temporary byte, rate, and concurrency limits.
- Disable the riskiest feature if no containment exists.
- Obtain vendor patch guidance.
- Stop automatic retries for parser budget failures.
- Begin monitoring CPU time per parsed byte.
During Patch Deployment
- Rebuild every relevant image.
- Verify image digests.
- Restart all workers.
- Confirm vendor backport evidence.
- Run the bounded local probe.
- Run staging integration tests.
- Verify timeout enforcement.
- Deploy gradually.
- Watch CPU, throttling, latency, and queue depth.
After Deployment
- Confirm old replicas are gone.
- Update SBOM and vulnerability records.
- Retain remediation evidence.
- Keep byte and CPU budgets.
- Add regression tests to CI.
- Review other untrusted parser paths.
- Revisit retry and autoscaling behavior.
- Document ownership of content-processing services.
Long-Term Parser Security Lessons
CVE-2026-15308 illustrates several broader engineering principles.
Streaming APIs Need Amortized Cost Analysis
A function may appear linear when called once but become quadratic when invoked repeatedly with retained state. Performance review should include the sequence of calls, not only the complexity of one invocation.
Questions for any incremental parser include:
- Does it rescan previously seen data?
- Does it copy the entire buffer on append?
- How does it behave when no progress is possible?
- Is pending state bounded?
- Does the retry interval grow?
- Can the caller influence fragmentation?
- What happens at end of input?
- Can the parser be cancelled?
Tolerant Parsing Expands the State Space
A parser that accepts imperfect markup must keep more possibilities open. It may wait for later input instead of rejecting an incomplete structure immediately.
That tolerance is useful for real-world HTML, but every waiting state needs a resource policy. The parser should avoid reconsidering unchanged prefixes unnecessarily, and the application should cap how long uncertainty is allowed to persist.
Input Validation Does Not Replace Resource Governance
A service may correctly validate that a field is text and still be vulnerable to algorithmic exhaustion. Content can be syntactically acceptable enough to enter the parser while remaining computationally pathological.
Security boundaries should include:
- Type validation
- Size validation
- Structural validation
- Time budgets
- CPU budgets
- Concurrency limits
- Cancellation
- Isolation
- पर्यवेक्षणीयता
Availability Bugs Need Reproducible Evidence
A CPU spike without request correlation is difficult to investigate. Logging only response status is insufficient.
Measure the work unit:
input bytes
chunk count
CPU seconds
wall seconds
tenant
content hash
parser version
result
Those fields allow defenders to distinguish a large legitimate document from a small disproportionately expensive one.
Library Fixes and Application Controls Serve Different Purposes
The upstream patch removes the specific quadratic behavior. Application controls protect against:
- Future parser defects
- Very large legitimate workloads
- Expensive handler callbacks
- Decompression bombs
- Retry storms
- Multi-tenant fairness failures
- Misconfigured crawlers
- Unexpected content types
Keep both layers.
Frequently Asked Questions
Is CVE-2026-15308 a remote code execution vulnerability
- No. The documented impact is CPU denial of service.
- The corrected PSF CVSS vector assigns no confidentiality or integrity impact.
- The public patch changes buffering and parsing frequency rather than repairing memory corruption or command execution.
- A report should not claim shell access, arbitrary code execution, data theft, or system takeover without separate evidence.
- The practical risk is worker exhaustion, latency, failed requests, queue backlog, or service unavailability.
Which Python versions are affected
- The current CVE record marks CPython versions below 3.15.0 as affected.
- Python 3.15 pre-release builds created before the July 4 patch should not be assumed safe merely because they use the 3.15 series name.
- Maintenance branches from Python 3.10 through 3.15 received or tracked backports.
- Python 3.14.6 and 3.13.14 were released before the fix was merged and their original upstream artifacts therefore predate it.
- Linux vendors may backport the patch without changing the visible upstream version.
- Confirm the installed package, vendor advisory, image digest, and runtime behavior rather than relying only on
python --version.
Does an application remain vulnerable if it calls feed only once
- The newly disclosed amplification specifically concerns incomplete input spanning many
feed()calls. - A one-shot call does not reproduce the same repeated incremental rescanning pattern.
- That substantially reduces exposure to this exact path but is not proof that an old parser is safe from every malformed-input issue.
- Other parser complexity vulnerabilities, including CVE-2025-6069, have affected different input behavior.
- Continue to patch the runtime and enforce total input and execution limits.
How can I confirm that my Linux vendor backported the fix
- Record the exact operating-system package build, not only the Python feature version.
- Check the vendor’s security advisory and package changelog for CVE-2026-15308, CPython issue 153030, or the relevant upstream commits.
- Verify that the enabled repository and installed architecture match the advisory.
- Restart or redeploy every process using the old interpreter.
- Record the resulting container digest or package version.
- Run a bounded behavioral regression test in an isolated environment.
- Contact the vendor when its public package notes are ambiguous.
Are WAF rules and body-size limits enough
- No. They can reduce exposure but do not repair the parser.
- A body-size limit controls bytes, while this issue is also sensitive to chunk count and repeated parser calls.
- One string pattern cannot reliably represent every incomplete parser state.
- Internal crawlers, queues, email processors, and background workers may bypass the WAF.
- Combine a verified patch with byte limits, chunk limits, authentication, rate limits, bounded concurrency, hard deadlines, and CPU isolation.
Can I safely enforce the timeout with a Python thread
- A thread-level timeout often stops waiting for a result but does not stop the CPU-bound thread.
- Python does not provide a general safe mechanism for forcefully killing an arbitrary running thread.
- Use a separate process, supervised worker, container, or job boundary that can be terminated.
- Test that the timeout actually stops resource consumption.
- Recycle a worker after a parser deadline so hidden state or unfinished work does not remain.
- Keep the worker pool bounded to prevent an attacker from filling it with timed-out jobs.
Is CVE-2026-15308 being exploited in the wild
- CISA’s SSVC metadata recorded exploitation as none on July 9, 2026.
- That field reflects the information available at that time, not a permanent guarantee that no attempts will occur.
- The condition was marked automatable, meaning repeated triggering can plausibly be automated once a reachable application path is known.
- Absence from known-exploitation reporting should not delay patching an exposed public parser.
- Review current vendor, CISA, and threat-intelligence information during remediation because exploitation status can change.
What evidence should a security team retain after remediation
- The affected service and parser call path
- Input trust and reachability analysis
- Original Python build and package version
- Vendor advisory or upstream patch mapping
- New package or image digest
- Deployment and process restart records
- Bounded pre-patch and post-patch test results
- CPU and wall-time measurements
- Temporary compensating controls
- Production rollout metrics
- Evidence that old workers and images were removed
- Final retest status and responsible owner
Closing Assessment
CVE-2026-15308 is a high-severity availability flaw in a widely available Python standard-library component. Its technical cause is narrow and understandable: an unfinished HTML structure could cause a growing buffer to be copied and rescanned after every small incremental feed, turning linear input growth into near-quadratic CPU work.
The highest-risk environments are public or multi-tenant services that stream uncontrolled HTML directly into HTMLParser, process it inside shared workers, and lack hard limits on bytes, chunk count, execution time, concurrency, retries, and CPU.
The upstream correction changes the parser’s scheduling strategy so unresolved buffers are joined and reconsidered much less frequently. Deploying a build that contains that change is the primary fix. Request limits, coalescing, process isolation, CPU quotas, fair scheduling, and detailed telemetry should remain as defense in depth.
The correct validation standard is evidence-based. Confirm the runtime, trace the actual parser path, establish the input boundary, verify the vendor backport or source fix, test only in a bounded isolated environment, and prove that the remediated workload no longer exhibits the old scaling. A version string alone is neither proof of exploitability nor proof of safety.

