---
title: "Eight Pydantic AI Security Advisories: What to Upgrade"
url: "https://nareshkumar.online/blog/pydantic-ai-security-advisories"
author: "Naresh Kumar"
published: "2026-10-10"
updated: "2026-10-10"
category: "Engineering"
tags: ["AI Agents", "LLMs", "Python", "Security"]
---

# Eight Pydantic AI Security Advisories: What to Upgrade

**In short:** Upgrade Pydantic AI to 2.53.0 or later (2.55.0 is current), or to 1.107.7 on the 1.x line, and upgrade clai as well. The two high-severity bugs hit the web chat UI (Agent.to_web(), clai web) and streaming through ConcurrencyLimitedModel. Run pip-audit with the OSV service to see them.

On October 8, 2026, GitHub's advisory database published eight advisories for [Pydantic AI](https://github.com/pydantic/pydantic-ai), the agent framework from the Pydantic team: two high, four medium and two low. The fixes themselves are older. The project published the web UI advisories in August and the streaming one on October 2, but scanners that read the global database learned about them only this week. If you serve an agent with `Agent.to_web()` or `clai web`, or stream through a concurrency-limited model, check your version today. I ran the two high-severity fixes side by side on old and new releases, and the results are below.

## What to install

| You run | Upgrade to | Current release |
| --- | --- | --- |
| Pydantic AI 2.x | 2.53.0 or later, which fixes all eight | 2.55.0 (October 9, 2026) |
| Pydantic AI 1.x | 1.107.7, which fixes the seven that affect 1.x | 1.107.7 (September 30, 2026) |
| `clai` | The same version as Pydantic AI | Each `clai` release pins its exact `pydantic-ai` version |

The 1.x line gets security fixes only. The [version policy](https://pydantic.dev/docs/ai/project/version-policy/) promises them "for at least 6 months after V2's stable release", which was June 23, 2026. `pydantic-ai` is a meta package that pins `pydantic-ai-slim`, so check the slim package when you look for the installed version:

```bash
python -m pip install -U "pydantic-ai>=2.53"     # or pydantic-ai-slim[...] if that is what you install
python -c "import importlib.metadata as m; print(m.version('pydantic-ai-slim'))"
uv tool upgrade clai                             # if you installed the CLI as a uv tool
```

## The eight advisories

| Advisory | Severity | What | Fixed in 2.x / 1.x |
| --- | --- | --- | --- |
| [GHSA-h4xc-3qfq-jf93](https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-h4xc-3qfq-jf93) | High, 7.6 | Web chat UI: a website you visit can start agent runs and tool calls | 2.28.0 / 1.107.4 |
| [GHSA-6fqq-452j-qhrp](https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-6fqq-452j-qhrp) | High, 7.5 | Streams through `ConcurrencyLimitedModel` keep their slot | 2.53.0 / not affected |
| [GHSA-q2xc-rrxj-58x9](https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-q2xc-rrxj-58x9) | Medium, 6.4 | Web chat UI does not check the `Host` header (DNS rebinding) | 2.30.0 / 1.107.5 |
| [GHSA-vmxc-h2x2-jmf3](https://github.com/advisories/GHSA-vmxc-h2x2-jmf3) | Medium, 6.8 | SSRF cloud-metadata blocklist bypass through IPv6 zone identifiers | 2.44.0 / 1.107.6 |
| [GHSA-fpf4-vwcp-v4hp](https://github.com/advisories/GHSA-fpf4-vwcp-v4hp) | Medium, 6.5 | `web_fetch` blocks the event loop with quadratic title extraction | 2.44.0 / 1.107.6 |
| [GHSA-v36g-jcw9-x7cw](https://github.com/advisories/GHSA-v36g-jcw9-x7cw) | Medium, 6.5 | Local web fetching uses excessive resources on nested HTML | 2.52.0 / 1.107.7 |
| [GHSA-3gh4-cghq-f8v4](https://github.com/advisories/GHSA-3gh4-cghq-f8v4) | Low | OpenTelemetry: retry prompts not redacted with `include_content=False` | 2.27.1 / 1.107.4 |
| [GHSA-4x9p-g9wm-8q7f](https://github.com/advisories/GHSA-4x9p-g9wm-8q7f) | Low | OpenTelemetry: exception events include content with `include_content=False` | 2.44.0 / 1.107.6 |

The CVE numbers are CVE-2026-107295, 107286, 107292, 107289, 107290, 107287, 107293 and 107291, in the order of the table. The rest of this post covers the three that come with a behavior change you should know about.

## The web chat UI: any open page could run your agent

`Agent.to_web()` returns a Starlette app with a chat interface, and `clai web` serves one on `127.0.0.1:7932` by default. Its chat endpoint did not check the request's content type. Browsers let a page send a few content types, such as plain text and form encodings, to another origin without asking that server first, so any page open in your browser could make your local server start an agent run. The agent's tools then ran with the privileges and credentials of your local process. The [advisory](https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-h4xc-3qfq-jf93) is explicit that binding to localhost "does not prevent this", and that tools marked `requires_approval=True` were not protected either, because the endpoint trusted approval decisions relayed by the client.

Since 2.28.0 and 1.107.4, `POST /api/chat` requires `Content-Type: application/json` and answers anything else with 415 before it parses the body. A browser must ask permission before it sends JSON to another origin, and the server now refuses that request. The bundled UI already sends the header. Your own scripts that call the endpoint must send it too.

### The second fix: the Host header

DNS rebinding reaches the same endpoint differently: a domain that first points at a real server is switched to `127.0.0.1`, so the browser treats your local UI as part of that site. Since 2.30.0 and 1.107.5, the app checks the `Host` header. It answers to `localhost`, names ending in `.localhost`, IP addresses (including LAN addresses) and hosts you allow, and replies 421 Misdirected Request to anything else. Behind a proxy or a tunnel, name the hosts you serve:

```python
app = agent.to_web(allowed_hosts=["ui.example.com"])
```

```bash
clai web --agent my_module:my_agent --allowed-host ui.example.com
```

### What I saw on each version

I sent two harmless requests to `Agent(TestModel()).to_web()` in-process through httpx, with no network and no real model: an empty `text/plain` POST to the chat endpoint, and a GET with a `Host` header that is not local.

| pydantic-ai-slim | Empty `text/plain` POST | `Host: example.test` |
| --- | --- | --- |
| 2.27.0 | Went on to parse the body (500 on my empty body) | 200 |
| 2.28.0 | 415 | 200 |
| 2.30.0 | 415 | 421 |
| 2.55.0 | 415 | 421 |

The 2.28.0 row is the reason to go past the first fix: it rejects the content type but still answers to any host name.

## Django already ships both defenses

Both bugs are old problems for web frameworks, and Django solves them by default. `CsrfViewMiddleware` rejects a cross-site POST without a token, and `ALLOWED_HOSTS` rejects unknown `Host` headers. With `DEBUG = True` and an empty `ALLOWED_HOSTS`, Django accepts only `.localhost`, `127.0.0.1` and `[::1]`, a stricter version of the rule Pydantic AI adopted. These tests pass on Django 6.1.2 against a plain JSON chat view:

`assistant/tests.py`

```python
from django.test import Client, SimpleTestCase, override_settings


class ChatEndpointTests(SimpleTestCase):
    def test_post_without_csrf_token_is_refused(self):
        client = Client(enforce_csrf_checks=True)
        response = client.post(
            "/chat/", '{"prompt": "hi"}', content_type="text/plain"
        )
        self.assertEqual(response.status_code, 403)

    @override_settings(DEBUG=True, ALLOWED_HOSTS=[])
    def test_unknown_host_is_refused(self):
        response = self.client.get("/chat/", headers={"host": "example.test"})
        self.assertEqual(response.status_code, 400)

    @override_settings(DEBUG=True, ALLOWED_HOSTS=[])
    def test_localhost_is_allowed(self):
        response = self.client.get("/chat/", headers={"host": "localhost:8000"})
        self.assertEqual(response.status_code, 405)  # the view accepts only POST
```

The trap is opting out. A `@csrf_exempt` on an agent endpoint "because the frontend calls it with fetch" puts your view where the old web UI was. In Django REST Framework, CSRF is checked only for requests authenticated by `SessionAuthentication`; the [docs](https://www.django-rest-framework.org/api-guide/authentication/) say anonymous requests "may be sent without CSRF tokens". An agent endpoint that runs tools should therefore require authentication. My post on [DRF authentication and permissions](https://nareshkumar.online/blog/drf-authentication-permissions-jwt) covers the setup.

## Streaming through a concurrency limit leaked slots

The second high-severity advisory affects 2.10.0 through 2.52.x when you wrap a model in `ConcurrencyLimitedModel` or `limit_model_concurrency()` and stream through it. Agent-level `max_concurrency`, non-streaming requests and the 1.x line are not affected. The limiter was built on anyio's `CapacityLimiter`, which ties each slot to the task that took it. Pydantic AI can run a stream's cleanup on a different task from the one that took the slot, so the release was refused and the slot stayed taken. The [2.53.0 release notes](https://github.com/pydantic/pydantic-ai/releases/tag/v2.53.0) say this happened after an early exit and "also after fully consuming `stream_text()` with its default debouncing". Once every slot leaks, all requests sharing the limiter wait.

This script shows it with one slot and the built-in test model:

`check_slots.py`

```python
import asyncio

from pydantic_ai import Agent, ConcurrencyLimiter
from pydantic_ai.models.concurrency import ConcurrencyLimitedModel
from pydantic_ai.models.test import TestModel

limiter = ConcurrencyLimiter(max_running=1)
agent = Agent(ConcurrencyLimitedModel(TestModel(), limiter=limiter))


async def main():
    try:
        async with agent.run_stream("hello") as result:
            async for _ in result.stream_text():
                pass
    except RuntimeError as exc:
        print("stream raised:", exc)
    print("free slots:", limiter.available_count)
    try:
        await asyncio.wait_for(agent.run("next request"), timeout=2)
        print("next request: ok")
    except TimeoutError:
        print("next request: still waiting after 2 s")


asyncio.run(main())
```

```text
# pydantic-ai-slim 2.52.0
stream raised: this borrower isn't holding any of this CapacityLimiter's tokens
free slots: 0
next request: still waiting after 2 s

# pydantic-ai-slim 2.53.0
free slots: 1
next request: ok
```

On 2.52.0 an ordinary, fully read stream was enough to lose the only slot. 2.53.0 switches to a semaphore, and the slot comes back.

> **Warning:** ⚠️ The fix in 2.53.0 also changes how limiters are shared. A model wrapper now raises `UserError` when it shares a limiter with the agent or with an enclosing model wrapper. `ConcurrencyLimiter.acquire()` takes a slot on every call, even on the same task. A custom `AbstractConcurrencyLimiter` must allow `release()` from another task. Run your tests after upgrading.

If you cannot upgrade yet, the advisory suggests agent-level `max_concurrency` instead, or not streaming through a limited model.

## Why your scanner may not show them yet

Each fix was announced in the project's own repository first: the two web UI advisories and one OpenTelemetry advisory on August 12 and 14, three more on September 17, one on September 30 and the streaming one on October 2. They reached the global GitHub Advisory Database, and through it OSV and Dependabot, on October 8. A scanner that reads PyPI's own vulnerability data may still be behind. On October 10, 2026, pip-audit 2.10.1 with its default service reported "No known vulnerabilities found" for `pydantic-ai-slim==2.27.0`, and with the OSV service it listed all eight:

```bash
pip-audit -r requirements.txt                                  # default service: nothing listed on October 10
pip-audit -r requirements.txt --vulnerability-service osv      # all eight listed
```

For dependencies that move this fast, I run pip-audit with `--vulnerability-service osv` in CI and watch the project's releases on GitHub. The same week also brought [Django 6.1.2 and DRF 3.18.3 security releases](https://nareshkumar.online/blog/django-6-1-2-security-release), and if you are moving to Python 3.15, [Lifeguard checks your Django project for lazy import problems](https://nareshkumar.online/blog/lifeguard-lazy-imports-django).

## Frequently asked questions

**Which Pydantic AI version fixes the October 2026 security advisories?**

Pydantic AI 2.53.0 or later fixes all eight on the 2.x line; 2.55.0 is the current release. On the 1.x line, 1.107.7 fixes the seven that affect 1.x. Upgrade clai to the same version, because each clai release pins its pydantic-ai version.

**Does binding the Pydantic AI web UI to 127.0.0.1 protect it?**

No. The advisory says binding to localhost does not prevent the attack, because a page open in your browser can reach the loopback address. Upgrade to 2.30.0 or 1.107.5 or later for both web UI fixes.

**Am I affected if I never use Agent.to\_web() or clai web?**

Not by the two web UI advisories. Check the others: streaming through ConcurrencyLimitedModel, the web\_fetch and local web fetching tools, and OpenTelemetry instrumentation with include\_content=False.

**Is Pydantic AI 1.x affected?**

By seven of the eight. The concurrency slot leak was introduced in 2.10.0 and never affected 1.x. Version 1.107.7 fixes the rest, and 1.x receives security fixes for at least six months after the June 23, 2026 release of 2.0.

**Why does pip-audit not report the Pydantic AI advisories?**

On October 10, 2026 the default PyPI vulnerability service did not list them yet. Run pip-audit with --vulnerability-service osv, which reads the OSV database that imported them on October 8.
