{"id":"GHSA-fpf4-vwcp-v4hp","summary":"Pydantic AI: Event loop blocked by quadratic title extraction in `web_fetch`","details":"### Summary\n\nThe 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.\n\nThis 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.\n\n### Details\n\nTitle 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.\n\nThe 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.\n\nSeparately, 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.\n\n### Who Is Affected\n\nYou 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.\n\n### Remediation\n\nUpgrade 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.\n\n### Credits\n\nReported 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.","aliases":["CVE-2026-107290"],"modified":"2026-10-08T17:00:11.837037037Z","published":"2026-10-08T16:48:25Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-1333","CWE-407"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-08T16:48:25Z"},"references":[{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-fpf4-vwcp-v4hp"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/pull/8397"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/pull/8399"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/pull/8418"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/pull/8433"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/pull/8434"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/commit/2faa6181d8a17d83bc9516d035c5270db8730fa0"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/commit/9cdc952e4c3319e85a3e04f2de49fbbb765bd38b"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/commit/a93ea5226be1e93ae13131ae3f22287190411389"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/commit/c3fd1cc1f15fdbf750d78e4e3ec1e8b4d6a3d920"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/commit/fb92ccfc3ca2735dab877e2ed73856681bf72ad1"},{"type":"PACKAGE","url":"https://github.com/pydantic/pydantic-ai"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/releases/tag/v1.107.6"},{"type":"WEB","url":"https://github.com/pydantic/pydantic-ai/releases/tag/v2.44.0"}],"affected":[{"package":{"name":"pydantic-ai","ecosystem":"PyPI","purl":"pkg:pypi/pydantic-ai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"1.77.0"},{"fixed":"1.107.6"}]}],"versions":["1.100.0","1.101.0","1.102.0","1.103.0","1.104.0","1.105.0","1.106.0","1.107.0","1.107.1","1.107.2","1.107.4","1.107.5","1.77.0","1.78.0","1.79.0","1.80.0","1.81.0","1.82.0","1.83.0","1.84.0","1.84.1","1.85.0","1.85.1","1.86.0","1.86.1","1.87.0","1.88.0","1.89.0","1.89.1","1.90.0","1.91.0","1.92.0","1.93.0","1.94.0","1.95.0","1.95.1","1.96.0","1.96.1","1.97.0","1.98.0","1.99.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-fpf4-vwcp-v4hp/GHSA-fpf4-vwcp-v4hp.json"}},{"package":{"name":"pydantic-ai","ecosystem":"PyPI","purl":"pkg:pypi/pydantic-ai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0b1"},{"fixed":"2.44.0"}]}],"versions":["2.0.0","2.0.0b1","2.0.0b2","2.0.0b3","2.0.0b4","2.0.0b5","2.0.0b6","2.0.0b7","2.1.0","2.10.0","2.11.0","2.12.0","2.13.0","2.14.0","2.14.1","2.15.0","2.16.0","2.17.0","2.18.0","2.19.0","2.2.0","2.20.0","2.21.0","2.22.0","2.23.0","2.24.0","2.25.0","2.26.0","2.27.0","2.27.1","2.28.0","2.29.0","2.3.0","2.30.0","2.31.0","2.31.1","2.32.0","2.32.1","2.32.2","2.33.0","2.34.0","2.35.0","2.35.1","2.35.3","2.36.0","2.37.0","2.38.0","2.39.0","2.4.0","2.40.0","2.41.0","2.42.0","2.43.0","2.5.0","2.5.1","2.6.0","2.7.0","2.8.0","2.9.0","2.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-fpf4-vwcp-v4hp/GHSA-fpf4-vwcp-v4hp.json"}},{"package":{"name":"pydantic-ai-slim","ecosystem":"PyPI","purl":"pkg:pypi/pydantic-ai-slim"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"1.77.0"},{"fixed":"1.107.6"}]}],"versions":["1.100.0","1.101.0","1.102.0","1.103.0","1.104.0","1.105.0","1.106.0","1.107.0","1.107.1","1.107.2","1.107.4","1.107.5","1.77.0","1.78.0","1.79.0","1.80.0","1.81.0","1.82.0","1.83.0","1.84.0","1.84.1","1.85.0","1.85.1","1.86.0","1.86.1","1.87.0","1.88.0","1.89.0","1.89.1","1.90.0","1.91.0","1.92.0","1.93.0","1.94.0","1.95.0","1.95.1","1.96.0","1.96.1","1.97.0","1.98.0","1.99.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-fpf4-vwcp-v4hp/GHSA-fpf4-vwcp-v4hp.json"}},{"package":{"name":"pydantic-ai-slim","ecosystem":"PyPI","purl":"pkg:pypi/pydantic-ai-slim"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0b1"},{"fixed":"2.44.0"}]}],"versions":["2.0.0","2.0.0b1","2.0.0b2","2.0.0b3","2.0.0b4","2.0.0b5","2.0.0b6","2.0.0b7","2.1.0","2.10.0","2.11.0","2.12.0","2.13.0","2.14.0","2.14.1","2.15.0","2.16.0","2.17.0","2.18.0","2.19.0","2.2.0","2.20.0","2.21.0","2.22.0","2.23.0","2.24.0","2.25.0","2.26.0","2.27.0","2.27.1","2.28.0","2.29.0","2.3.0","2.30.0","2.31.0","2.31.1","2.32.0","2.32.1","2.32.2","2.33.0","2.34.0","2.35.0","2.35.1","2.35.3","2.36.0","2.37.0","2.38.0","2.39.0","2.4.0","2.40.0","2.41.0","2.42.0","2.43.0","2.5.0","2.5.1","2.6.0","2.7.0","2.8.0","2.9.0","2.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-fpf4-vwcp-v4hp/GHSA-fpf4-vwcp-v4hp.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"}]}