CVE-2026-107290: Pydantic AI: Event loop blocked by quadratic title extraction in `web_fetch`
Pydantic AI's local `web_fetch_tool` ran several steps whose time grows with the square of server-controlled input, on the event loop: charset decoding, a backtracking title regex, and HTML-to-Markdown conversion. A page of a megabyte or two can block the whole process for minutes. Availability only. Fixed in 1.107.6 (1.x) and 2.44.0 (2.x).
Published Oct 8, 2026 · Analysis Oct 21, 2026
What an attacker can do
Title extraction used a backtracking pattern over the raw body, so repeated unterminated tag openings cost time proportional to the square of the size. Three conversion steps were quadratic too: whitespace normalization, stripping preformatted blocks, and numbering ordered lists. The regex steps hold the interpreter lock even when moved off the event loop.
Decoding used the codec named in the response's Content-Type charset, looked up in Python's codec registry. A body of about 1 MB labelled charset=punycode blocked the event loop for roughly half a minute with no HTML needed. Codecs like rot_13 and base64_codec raised an unexpected exception that aborted the agent run. Pages nested a few hundred elements deep raised RecursionError with the same effect on that one run.
The attacker needs a way to make the agent fetch their page, through an untrusted prompt, and a low-privilege account per the vector. Every coroutine in the process stalls, including other agent runs and served requests. The earlier download size limit bounds bytes, not processing time. allowed_domains narrows exposure but does not remove it.
Who is affected
Affected: pydantic-ai and pydantic-ai-slim from 1.77.0 up to 1.107.6, and from 2.0.0b1 up to 2.44.0, if your app registers web_fetch_tool or relies on the WebFetch capability's local fallback and exposes the agent to untrusted prompts.
pip show pydantic-ai pydantic-ai-slimApps that fetch only developer-controlled URLs are not exposed to the model-chosen path.
pydantic-ai
- Ecosystem
- PyPI
- Affected
- >= 1.77.0, < 1.107.6
- Fixed in
- 1.107.6
- Check
pip show pydantic-ai
pydantic-ai
- Ecosystem
- PyPI
- Affected
- >= 2.0.0b1, < 2.44.0
- Fixed in
- 2.44.0
- Check
pip show pydantic-ai
pydantic-ai-slim
- Ecosystem
- PyPI
- Affected
- >= 1.77.0, < 1.107.6
- Fixed in
- 1.107.6
- Check
pip show pydantic-ai-slim
pydantic-ai-slim
- Ecosystem
- PyPI
- Affected
- >= 2.0.0b1, < 2.44.0
- Fixed in
- 2.44.0
- Check
pip show pydantic-ai-slim
How to fix it
Upgrade to 1.107.6 or 2.44.0:
pip install --upgrade pydantic-aiThe title is now found with a single linear scan, the conversion steps run in linear time, and decoding, title extraction and conversion run in a worker thread. A charset naming a non-text codec, or a page too deeply nested, is reported to the model as a failed fetch instead of aborting the run. A JSON body nested too deeply is returned as plain text. The advisory gives no other workaround.
How to tell if you were hit
Look for event loop stalls: health checks timing out while the process is busy, all agent runs pausing at the same time, and a fetch of one URL that precedes the stall. Check fetched responses with charset=punycode in Content-Type. Watch for RecursionError or codec exceptions escaping web_fetch in logs.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Summary
The local web-fetch tool (web_fetch_tool, also used as the WebFetch capability's local fallback) processed responses with several steps whose running time grows quadratically with the size of certain server-controlled inputs, and ran them on the event loop: decoding the body with whichever charset the server declared, extracting the page title with a backtracking regular expression, and converting the HTML to markdown. An application that exposes this tool to untrusted prompts can be steered to fetch an attacker-controlled page of a megabyte or two that blocks the event loop for minutes, stalling every other coroutine in the process — other agent runs, other requests being served — for the duration.
This is an availability issue only. SSRF protections and the download size limit introduced in GHSA-v2xh-2vp8-57h8 are unaffected; that limit bounds how much is downloaded, not how long the response takes to process.
Details
Title extraction used a backtracking pattern over the raw response body, so a body made of repeated unterminated tag openings cost time proportional to the square of its size. The HTML-to-markdown conversion had the same shape in three of its steps: normalizing whitespace, stripping preformatted blocks, and numbering ordered lists all took time proportional to the square of a run of spaces or a list's length. All of it ran on the event loop, and the regex steps hold the interpreter lock even when moved off it, so the whole process paid for the size of a server-controlled response.
The response body was also decoded on the event loop with the codec named by the charset parameter of the response's Content-Type, looked up in Python's codec registry. That registry includes punycode, whose decoder takes time proportional to the square of its input: a response of about one megabyte labelled charset=punycode blocked the event loop for roughly half a minute, with no HTML required. The registry also includes codecs that aren't text encodings at all, such as rot_13 and base64_codec; a response labelled with one of those raised an unexpected exception out of the tool, aborting the agent run that fetched it.
Separately, the HTML-to-markdown conversion recursed once per nested element, so a page nested a few hundred elements deep raised a RecursionError out of the tool, aborting the agent run that fetched it. A JSON response nested deeper than the interpreter allows did the same. These only affect that one run.
Who Is Affected
You are affected if your application registers the local web-fetch tool (or relies on the WebFetch capability's local fallback) and exposes the agent to untrusted prompts. allowed_domains narrows the exposure to pages on those domains but does not remove it. Applications that only fetch developer-controlled URLs are not exposed to the model-chosen attack path.
Remediation
Upgrade to a patched version. The title is now found with a single linear scan, the conversion steps above run in linear time, and decoding, title extraction and conversion all run in a worker thread. A charset naming a codec that isn't a text encoding, and a page too deeply nested to convert, are reported back to the model as a failed fetch instead of aborting the run; a JSON body too deeply nested to parse is returned as plain text.
Credits
Reported privately by @BrianWillows, whose report covered the quadratic title extraction. The response decoding, the codecs that are not text encodings, and the quadratic steps in the HTML-to-markdown conversion were found while fixing it.
Credited to BrianWillows.
Questions
- Is the 50 MiB download cap enough to prevent this?
No. The size limit introduced earlier bounds bytes downloaded, not how long processing takes. A response of one or two megabytes was enough here.
- Why can `charset=punycode` hang the process?
Python's codec registry includes a punycode decoder with quadratic time. A one-megabyte body labelled with it blocked the event loop for about half a minute.
- Does `allowed_domains` remove the risk?
No. It limits fetches to pages on those domains but does not remove the exposure, since a page on an allowed domain may still be attacker-controlled.
References
Read for this analysis:
- https://github.com/advisories/GHSA-fpf4-vwcp-v4hp
- https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-fpf4-vwcp-v4hp
- https://github.com/pydantic/pydantic-ai/pull/8397
- https://github.com/pydantic/pydantic-ai/pull/8399
- https://github.com/pydantic/pydantic-ai/pull/8418
- https://github.com/pydantic/pydantic-ai/pull/8433
- https://github.com/pydantic/pydantic-ai/pull/8434
- https://github.com/pydantic/pydantic-ai/commit/2faa6181d8a17d83bc9516d035c5270db8730fa0
- https://github.com/pydantic/pydantic-ai/commit/9cdc952e4c3319e85a3e04f2de49fbbb765bd38b
- https://github.com/pydantic/pydantic-ai/commit/a93ea5226be1e93ae13131ae3f22287190411389
- https://github.com/pydantic/pydantic-ai/commit/c3fd1cc1f15fdbf750d78e4e3ec1e8b4d6a3d920
- https://github.com/pydantic/pydantic-ai/commit/fb92ccfc3ca2735dab877e2ed73856681bf72ad1
- https://github.com/pydantic/pydantic-ai/releases/tag/v1.107.6
- https://github.com/pydantic/pydantic-ai/releases/tag/v2.44.0
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More Pydantic AI vulnerabilities
All Pydantic AI entriesCVE-2026-107286
Pydantic AI: Concurrency-limited models can keep their slot when a streamed request ends early
high7.5Oct 8, 2026CVE-2026-107287
Pydantic AI: Excessive resource use when local web fetching converts nested HTML
medium6.5Oct 8, 2026CVE-2026-107289
Pydantic AI: SSRF cloud-metadata blocklist bypass via IPv6 zone identifiers
medium6.8Oct 8, 2026CVE-2026-107291
Pydantic AI OpenTelemetry instrumentation: exception events on tool and agent run spans include content when `include_content=False`
low2.3Oct 8, 2026CVE-2026-107292
Pydantic AI Web chat UI (`Agent.to_web()`, `clai web`): the local chat endpoint does not validate the `Host` header
medium6.4Oct 8, 2026CVE-2026-107293
Pydantic AI OpenTelemetry instrumentation: retry prompt content is not redacted when `include_content=False`
low2.3Oct 8, 2026
