pydantic-ai: Pydantic AI: Event loop blocked by quadratic title extraction in `web_fetch`
What fixes it
- 1.107.6pydantic-ai, pipGitHub
- 2.44.0pydantic-ai, pipGitHub
- 1.107.6pydantic-ai-slim, pipGitHub
- 2.44.0pydantic-ai-slim, pipGitHub
- 1.107.6pydantic-ai, PyPIOSV
pip install "pydantic-ai>=1.107.6" - 2.44.0pydantic-ai, PyPIOSV
pip install "pydantic-ai>=2.44.0"
Show all 8 fixed versions
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
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.
Text from GitHub Advisory Database
Affected versions 194
- pydantic-ai, pip>= 1.77.0, < 1.107.6GitHub
- pydantic-ai, pip>= 2.0.0b1, < 2.44.0GitHub
- pydantic-ai-slim, pip>= 1.77.0, < 1.107.6GitHub
- pydantic-ai-slim, pip>= 2.0.0b1, < 2.44.0GitHub
- pydantic-aiaffected: >= 1.77.0, < 1.107.6CVE List
- pydantic-aiaffected: >= 2.0.0b1, < 2.44.0CVE List
- pydantic-ai-slimaffected: >= 1.77.0, < 1.107.6CVE List
- pydantic-ai-slimaffected: >= 2.0.0b1, < 2.44.0CVE List
- pydantic-ai, PyPIfrom 1.77.0, fixed in 1.107.6OSV
- pydantic-ai, PyPI1.100.0OSV
- pydantic-ai, PyPI1.101.0OSV
- pydantic-ai, PyPI1.102.0OSV
- pydantic-ai, PyPI1.103.0OSV
- pydantic-ai, PyPI1.104.0OSV
- pydantic-ai, PyPI1.105.0OSV
- pydantic-ai, PyPI1.106.0OSV
- pydantic-ai, PyPI1.107.0OSV
- pydantic-ai, PyPI1.107.1OSV
- pydantic-ai, PyPI1.107.2OSV
- pydantic-ai, PyPI1.107.4OSV
- pydantic-ai, PyPI1.107.5OSV
- pydantic-ai, PyPI1.77.0OSV
- pydantic-ai, PyPI1.78.0OSV
- pydantic-ai, PyPI1.79.0OSV
- pydantic-ai, PyPI1.80.0OSV
- pydantic-ai, PyPI1.81.0OSV
- pydantic-ai, PyPI1.82.0OSV
- pydantic-ai, PyPI1.83.0OSV
- pydantic-ai, PyPI1.84.0OSV
- pydantic-ai, PyPI1.84.1OSV
- pydantic-ai, PyPI1.85.0OSV
- pydantic-ai, PyPI1.85.1OSV
- pydantic-ai, PyPI1.86.0OSV
- pydantic-ai, PyPI1.86.1OSV
- pydantic-ai, PyPI1.87.0OSV
- pydantic-ai, PyPI1.88.0OSV
- pydantic-ai, PyPI1.89.0OSV
- pydantic-ai, PyPI1.89.1OSV
- pydantic-ai, PyPI1.90.0OSV
- pydantic-ai, PyPI1.91.0OSV
- pydantic-ai, PyPI1.92.0OSV
- pydantic-ai, PyPI1.93.0OSV
- pydantic-ai, PyPI1.94.0OSV
- pydantic-ai, PyPI1.95.0OSV
- pydantic-ai, PyPI1.95.1OSV
- pydantic-ai, PyPI1.96.0OSV
- pydantic-ai, PyPI1.96.1OSV
- pydantic-ai, PyPI1.97.0OSV
- pydantic-ai, PyPI1.98.0OSV
- pydantic-ai, PyPI1.99.0OSV
- pydantic-ai, PyPIfrom 2.0.0b1, fixed in 2.44.0OSV
- pydantic-ai, PyPI2.0.0OSV
- pydantic-ai, PyPI2.0.0b1OSV
- pydantic-ai, PyPI2.0.0b2OSV
- pydantic-ai, PyPI2.0.0b3OSV
- pydantic-ai, PyPI2.0.0b4OSV
- pydantic-ai, PyPI2.0.0b5OSV
- pydantic-ai, PyPI2.0.0b6OSV
- pydantic-ai, PyPI2.0.0b7OSV
- pydantic-ai, PyPI2.1.0OSV
- pydantic-ai, PyPI2.10.0OSV
- pydantic-ai, PyPI2.11.0OSV
- pydantic-ai, PyPI2.12.0OSV
- pydantic-ai, PyPI2.13.0OSV
- pydantic-ai, PyPI2.14.0OSV
- pydantic-ai, PyPI2.14.1OSV
- pydantic-ai, PyPI2.15.0OSV
- pydantic-ai, PyPI2.16.0OSV
- pydantic-ai, PyPI2.17.0OSV
- pydantic-ai, PyPI2.18.0OSV
- pydantic-ai, PyPI2.19.0OSV
- pydantic-ai, PyPI2.2.0OSV
- pydantic-ai, PyPI2.20.0OSV
- pydantic-ai, PyPI2.21.0OSV
- pydantic-ai, PyPI2.22.0OSV
- pydantic-ai, PyPI2.23.0OSV
- pydantic-ai, PyPI2.24.0OSV
- pydantic-ai, PyPI2.25.0OSV
- pydantic-ai, PyPI2.26.0OSV
- pydantic-ai, PyPI2.27.0OSV
- pydantic-ai, PyPI2.27.1OSV
- pydantic-ai, PyPI2.28.0OSV
- pydantic-ai, PyPI2.29.0OSV
- pydantic-ai, PyPI2.3.0OSV
- pydantic-ai, PyPI2.30.0OSV
- pydantic-ai, PyPI2.31.0OSV
- pydantic-ai, PyPI2.31.1OSV
- pydantic-ai, PyPI2.32.0OSV
- pydantic-ai, PyPI2.32.1OSV
- pydantic-ai, PyPI2.32.2OSV
- pydantic-ai, PyPI2.33.0OSV
- pydantic-ai, PyPI2.34.0OSV
- pydantic-ai, PyPI2.35.0OSV
- pydantic-ai, PyPI2.35.1OSV
- pydantic-ai, PyPI2.35.3OSV
- pydantic-ai, PyPI2.36.0OSV
- pydantic-ai, PyPI2.37.0OSV
- pydantic-ai, PyPI2.38.0OSV
- pydantic-ai, PyPI2.39.0OSV
- pydantic-ai, PyPI2.4.0OSV
- pydantic-ai, PyPI2.40.0OSV
- pydantic-ai-slim, PyPIfrom 1.77.0, fixed in 1.107.6OSV
- pydantic-ai-slim, PyPI1.100.0OSV
- pydantic-ai-slim, PyPI1.101.0OSV
- pydantic-ai-slim, PyPI1.102.0OSV
- pydantic-ai-slim, PyPI1.103.0OSV
- pydantic-ai-slim, PyPI1.104.0OSV
- pydantic-ai-slim, PyPI1.105.0OSV
- pydantic-ai-slim, PyPI1.106.0OSV
- pydantic-ai-slim, PyPI1.107.0OSV
- pydantic-ai-slim, PyPI1.107.1OSV
- pydantic-ai-slim, PyPI1.107.2OSV
- pydantic-ai-slim, PyPI1.107.4OSV
- pydantic-ai-slim, PyPI1.107.5OSV
- pydantic-ai-slim, PyPI1.77.0OSV
- pydantic-ai-slim, PyPI1.78.0OSV
- pydantic-ai-slim, PyPI1.79.0OSV
- pydantic-ai-slim, PyPI1.80.0OSV
- pydantic-ai-slim, PyPI1.81.0OSV
- pydantic-ai-slim, PyPI1.82.0OSV
- pydantic-ai-slim, PyPI1.83.0OSV
- pydantic-ai-slim, PyPI1.84.0OSV
- pydantic-ai-slim, PyPI1.84.1OSV
- pydantic-ai-slim, PyPI1.85.0OSV
- pydantic-ai-slim, PyPI1.85.1OSV
- pydantic-ai-slim, PyPI1.86.0OSV
- pydantic-ai-slim, PyPI1.86.1OSV
- pydantic-ai-slim, PyPI1.87.0OSV
- pydantic-ai-slim, PyPI1.88.0OSV
- pydantic-ai-slim, PyPI1.89.0OSV
- pydantic-ai-slim, PyPI1.89.1OSV
- pydantic-ai-slim, PyPI1.90.0OSV
- pydantic-ai-slim, PyPI1.91.0OSV
- pydantic-ai-slim, PyPI1.92.0OSV
- pydantic-ai-slim, PyPI1.93.0OSV
- pydantic-ai-slim, PyPI1.94.0OSV
- pydantic-ai-slim, PyPI1.95.0OSV
- pydantic-ai-slim, PyPI1.95.1OSV
- pydantic-ai-slim, PyPI1.96.0OSV
- pydantic-ai-slim, PyPI1.96.1OSV
- pydantic-ai-slim, PyPI1.97.0OSV
- pydantic-ai-slim, PyPI1.98.0OSV
- pydantic-ai-slim, PyPI1.99.0OSV
- pydantic-ai-slim, PyPIfrom 2.0.0b1, fixed in 2.44.0OSV
- pydantic-ai-slim, PyPI2.0.0OSV
- pydantic-ai-slim, PyPI2.0.0b1OSV
- pydantic-ai-slim, PyPI2.0.0b2OSV
- pydantic-ai-slim, PyPI2.0.0b3OSV
- pydantic-ai-slim, PyPI2.0.0b4OSV
- pydantic-ai-slim, PyPI2.0.0b5OSV
- pydantic-ai-slim, PyPI2.0.0b6OSV
- pydantic-ai-slim, PyPI2.0.0b7OSV
- pydantic-ai-slim, PyPI2.1.0OSV
- pydantic-ai-slim, PyPI2.10.0OSV
- pydantic-ai-slim, PyPI2.11.0OSV
- pydantic-ai-slim, PyPI2.12.0OSV
- pydantic-ai-slim, PyPI2.13.0OSV
- pydantic-ai-slim, PyPI2.14.0OSV
- pydantic-ai-slim, PyPI2.14.1OSV
- pydantic-ai-slim, PyPI2.15.0OSV
- pydantic-ai-slim, PyPI2.16.0OSV
- pydantic-ai-slim, PyPI2.17.0OSV
- pydantic-ai-slim, PyPI2.18.0OSV
- pydantic-ai-slim, PyPI2.19.0OSV
- pydantic-ai-slim, PyPI2.2.0OSV
- pydantic-ai-slim, PyPI2.20.0OSV
- pydantic-ai-slim, PyPI2.21.0OSV
- pydantic-ai-slim, PyPI2.22.0OSV
- pydantic-ai-slim, PyPI2.23.0OSV
- pydantic-ai-slim, PyPI2.24.0OSV
- pydantic-ai-slim, PyPI2.25.0OSV
- pydantic-ai-slim, PyPI2.26.0OSV
- pydantic-ai-slim, PyPI2.27.0OSV
- pydantic-ai-slim, PyPI2.27.1OSV
- pydantic-ai-slim, PyPI2.28.0OSV
- pydantic-ai-slim, PyPI2.29.0OSV
- pydantic-ai-slim, PyPI2.3.0OSV
- pydantic-ai-slim, PyPI2.30.0OSV
- pydantic-ai-slim, PyPI2.31.0OSV
- pydantic-ai-slim, PyPI2.31.1OSV
- pydantic-ai-slim, PyPI2.32.0OSV
- pydantic-ai-slim, PyPI2.32.1OSV
- pydantic-ai-slim, PyPI2.32.2OSV
- pydantic-ai-slim, PyPI2.33.0OSV
- pydantic-ai-slim, PyPI2.34.0OSV
- pydantic-ai-slim, PyPI2.35.0OSV
- pydantic-ai-slim, PyPI2.35.1OSV
- pydantic-ai-slim, PyPI2.35.3OSV
- pydantic-ai-slim, PyPI2.36.0OSV
- pydantic-ai-slim, PyPI2.37.0OSV
- pydantic-ai-slim, PyPI2.38.0OSV
- pydantic-ai-slim, PyPI2.39.0OSV
- pydantic-ai-slim, PyPI2.4.0OSV
- pydantic-ai-slim, PyPI2.40.0OSV
References 15
- github.com/pydantic/pydantic-ai/commit/2faa6181d8a17d83bc9516d035c5270db8730fa0Fixvia CVE List
- github.com/pydantic/pydantic-ai/commit/9cdc952e4c3319e85a3e04f2de49fbbb765bd38bFixvia CVE List
- github.com/pydantic/pydantic-ai/commit/a93ea5226be1e93ae13131ae3f22287190411389Fixvia CVE List
- github.com/pydantic/pydantic-ai/commit/c3fd1cc1f15fdbf750d78e4e3ec1e8b4d6a3d920Fixvia CVE List
- github.com/pydantic/pydantic-ai/commit/fb92ccfc3ca2735dab877e2ed73856681bf72ad1Fixvia CVE List
- github.com/pydantic/pydantic-ai/releases/tag/v1.107.6Fixvia CVE List
- github.com/pydantic/pydantic-ai/releases/tag/v2.44.0Fixvia CVE List
- github.com/pydantic/pydantic-ai/security/advisories/GHSA-fpf4-vwcp-v4hpAdvisoryvia CVE List
- github.com/advisories/GHSA-fpf4-vwcp-v4hpAdvisoryvia GitHub
- github.com/pydantic/pydantic-ai/pull/8397Referencevia CVE List
- github.com/pydantic/pydantic-ai/pull/8399Referencevia CVE List
- github.com/pydantic/pydantic-ai/pull/8418Referencevia CVE List
- github.com/pydantic/pydantic-ai/pull/8433Referencevia GitHub
- github.com/pydantic/pydantic-ai/pull/8434Referencevia CVE List
- github.com/pydantic/pydantic-ai/pull/84332Referencevia CVE List