NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Pydantic AI

CVE-2026-107290: Pydantic AI: Event loop blocked by quadratic title extraction in `web_fetch`

mediumCVSS 3.1 6.5GHSA-fpf4-vwcp-v4hp

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-slim

Apps 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-ai

The 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

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 entries