Back to blog

Early Found an Unreported Security Bug in Pydantic AI

Not a replay of a known bug. Early Release Guard found an unreported security bug, we disclosed it privately, and Pydantic fixed it and published advisory GHSA-xv4x-g8mx-ch56.

Pydantic AI security case file: MCP credentials copied into traces, labeled with Early investigation priority P1.

Early Release Guard found an unreported security bug in Pydantic AI that had been present for about a year, while analyzing a later set of changes. When an agent used a remote MCP server with a token, the token and the request headers were written in clear text into OpenTelemetry traces. Turning off content capture did not remove them.

This is not a replay. Our earlier case files replayed bugs from project history. This one was live: it had not been reported, and it was present in every release since 1.4.0, 189 in total.

Early then created a Fix PR, with a test to help protect against the same bug returning. We reported the bug privately to the Pydantic maintainers, through a GitHub security advisory, with a reproduction and the proposed fix. Pydantic fixed it in Pydantic AI 2.58.0 and 1.107.8, released on October 20, 2026, and published the advisory the same day. We kept the details private until then, and wrote this only after the fix and the advisory were public.

At a glance

  • October 15, 2026: Early Release Guard found the bug. We reported it privately to Pydantic the same day.
  • October 20, 2026: Pydantic released fixes for both supported release lines and published advisory GHSA-xv4x-g8mx-ch56.
  • After that: this post.

This is what Find. Fix. Know. looked like on a real codebase, and what to check in your own tracing setup.

What leaked

Pydantic AI is the Pydantic team’s Python framework for building AI agents. Its MCPServerTool lets a model provider such as OpenAI or Anthropic call a remote MCP server directly. To reach a server that requires authentication, the tool carries an authorization_token and optional HTTP headers.

Pydantic AI can trace every model request with OpenTelemetry. Each request span carries an attribute called model_request_parameters, a serialized copy of the settings sent to the model. That copy included the MCP tool as a whole, credentials and all.

So anyone who could read the traces could read the token. That includes the tracing vendor, colleagues with dashboard access, and anywhere traces were exported or stored.

Why the privacy setting missed it

Pydantic AI has a setting for this exact worry. Its documentation says that with include_content=False, Pydantic AI excludes sensitive content from telemetry, including prompts, completions, tool arguments and other message content.

The setting removed the content it was designed to remove: the instructions the user wrote and the template for structured output. It never treated credentials as something to remove, because credentials are not content. They are configuration. The serializer copied the whole tool configuration, and nothing between the tool and the exporter asked whether a field was a secret.

That is why the defect survived. Tests for this area checked that prompt text did not reach traces, and they passed. No instrumentation test checked the token. The token arrived with a new tool in October 2025, version 1.4.0, through a generic path that already worked for every other setting.

This was not a regression. Nothing that once worked broke. It was an existing defect, present from the day the feature shipped, which is exactly the kind of bug a regression-only check is not looking for.

Find

We regularly run Early Release Guard on forks of several open-source projects, including Pydantic AI, to see what it finds on real codebases. This finding came from one of those runs, on current code.

The run covered changes to the Pydantic AI main branch between September 15 and October 1, 2026, 151 changed files. Early assessed 92 behavior changes and raised 9 of them as findings. This one ranked first: the highest score in the run, and the only finding the investigation rated P1. Early analyzes the code changes and their impact, and looks for new bugs, existing defects and regressions.

Early flagged this finding as an existing defect, older than the changes it was analyzing, and described the mechanism precisely. Credentials on MCPServerTool were serialized into the span attribute, and the redaction applied with content capture off did not cover them.

Fix

An investigation confirmed the finding. Early then created a Fix PR after authorized initiation, on our fork, not on the Pydantic repository.

The proposed change was small. Before the request settings are serialized for tracing, it replaces the token and every header value with [REDACTED]. The keys stay, so a trace still shows that credentials were sent. The settings sent to the model are unchanged.

The model request retains its credentials while the copy exported to traces redacts them.

Credentials remain in the model request. They are masked in the copy exported to traces.

Early also added a test for the affected behavior, checking both settings of include_content, to help protect the code from this bug returning. This is the core of the test Early wrote, as submitted:

@pytest.mark.parametrize('include_content', [True, False])
def test_mcp_server_credentials_never_reach_telemetry(include_content: bool) -> None:
    """MCP server tokens and headers are secrets, not content, so they are masked either way."""
    tool = MCPServerTool(
        id='docs',
        url='https://mcp.example.com',
        authorization_token='SENTINEL-mcp-token',
        headers={'X-Api-Key': 'SENTINEL-mcp-header'},
    )
    exported = model_request_parameters_attributes(
        ModelRequestParameters(native_tools=[tool]), include_content=include_content
    )
    serialized = str(exported['model_request_parameters'])
    assert 'SENTINEL-mcp-token' not in serialized
    assert 'SENTINEL-mcp-header' not in serialized
    # The request the model receives keeps its credentials.
    assert tool.authorization_token == 'SENTINEL-mcp-token'

Pydantic wrote its own fix, and it went further than ours.

Know

We checked what the fix covered, before and after Pydantic released its own:

  • On the latest Pydantic AI code, the defect still reproduced, and the patch applied cleanly and removed both the token and the header values from the trace.
  • The defect was present in 189 releases: v1.4.0 through v1.107.7, and v2.0.0 through v2.57.0.
  • After the fix shipped, we re-ran the reproduction on Pydantic AI 2.58.0. The token, the header values and the server URL are all redacted.

Private disclosure

A leaked credential is a security issue, so it did not go to a public pull request. Pydantic asks that security issues be reported privately, through GitHub’s advisory process. We submitted the report there with the reproduction, the affected versions and the proposed patch.

The advisory is GHSA-xv4x-g8mx-ch56. It is rated Medium, CVSS 6.5, and classified as CWE-532, insertion of sensitive information into a log file. The Pydantic maintainers confirmed it and fixed both supported release lines, in pull requests #10662 and #10663. They moved fast: five days from report to released fixes on both supported release lines. Their fix also went further than our report. It also redacts the MCP server URL and identifiers derived from it. One exception: on v2, Temporal activity names that use those identifiers are kept, so in-flight workflows do not break. The advisory credits the report.

  • October 2025: MCPServerTool added with an authorization_token field, first released in Pydantic AI v1.4.0.
  • October 15, 2026: Early found the defect. We confirmed it on the latest upstream code and reported it privately the same day.
  • October 20, 2026: Fixed in Pydantic AI 2.58.0 and 1.107.8. Advisory published.

Between the report and the release, we said nothing in public about the bug. This post came after both were out.

What to check in your own stack

If you use Pydantic AI with MCPServerTool and tracing:

  1. Upgrade to Pydantic AI 2.58.0 or later on v2, or 1.107.8 or later on v1.
  2. Search your stored traces for the model_request_parameters attribute. If it contains real tokens, treat those tokens as exposed.
  3. Rotate any MCP server credential that appeared in traces.
  4. On v2 with Temporal, if you run a URL-based MCP capability with tracing on, also pass an explicit id= to MCP. The fix keeps the existing activity names, which are derived from the URL. Make the change once in-flight workflows complete, because it renames the activities.

The broader lesson applies to any tracing setup. A content setting covers content. Secrets need their own redaction, and a test that proves it, because the next secret will arrive through a path that already works.

What this shows

Finding is not enough on its own. A list of findings leaves the work with the team. Here the finding arrived with a reviewable fix, a test, and a clear record of what was checked.

The defect was a year old, and Early found it while analyzing later changes, not only the newest diff. That is the difference between checking a change and guarding a release.

On this page

Read next

Could Early Have Flagged the PHP2Shell Bug in January 2025?
Could Early Have Flagged the PHP2Shell Bug in January 2025?
A historical PHP change, an unsafe memory lifetime, and a Fix PR engineers can inspect.
AI Bug Fixing: From Finding to Fix Pull Request
AI Bug Fixing: From Finding to Fix Pull Request
Give QA a path from finding to repair. Give engineering a concrete fix to review.
Beyond Code Review: What Makes a Release Ready?
Beyond Code Review: What Makes a Release Ready?
Over 50% of estimated implementation effort went to repairs. What does that mean for release confidence?