Security triage: mcp 2.x, LangChain 1.x, Go floor, verified-token audience, npm lockfiles - #432
Conversation
The alpha publish run failed with nothing but `curl: (22) 403`: the response body, which is where Play says whether the service account is missing from the console or the app has not been created yet, went into the jq that reads the edit id and was lost. Capture the body and status first and print the body on stderr for any non-2xx.
Dependabot's python group (google#426) raises mcp to 2.2.0, which renames FastMCP to MCPServer, moves host/port from the constructor to run(), and swaps httpx for httpx2 on the client side. The calc and greeter servers and the sam-mcp SDK were still written against 1.x, which is why the e2e suite on that PR could not start the calc backend. The SDK keeps returning wire-format dicts (inputSchema, isError): mcp 2.x fields are snake_case in Python, so the dumps go through by_alias. The remaining floors (openai 3, a2a-sdk, starlette, uvicorn, pytest, setuptools) are taken as dependabot proposed them.
The langchain 0.3 pin existed because 1.x removed AgentExecutor and create_tool_calling_agent; it is also the line the open PYSEC-2026 advisories are against, so the pin has to go. create_agent is the replacement: it runs the same model/tool loop, the persona stays in the human turn for models that refuse a system role, and the recursion limit maps the old max_iterations (one iteration is a model step and a tool step). Streaming the graph updates gives back what verbose=True printed: the tool calls and their results, which are the point of a chaos run. Smoke-tested one round against the calc MCP server and a stub OpenAI-compatible endpoint.
Scorecard reads the go directive as the stdlib version, and 1.26.0 is inside the affected range for both x/mod sumdb advisories (fixed in 1.25.13 and 1.26.6). CI already resolves 1.26 to a newer patch and the images build on 1.27.1, so this only moves the declared floor.
There was a problem hiding this comment.
Code Review
This pull request updates dependencies across several modules, refactors JWT verification in internal/identity/oidc.go, and transitions the chaos agent and MCP servers to newer SDK APIs. However, several critical issues were identified in the changes: the non-existent httpx2 library and MCPServer class were incorrectly introduced, and the chaos agent attempts to use a non-existent create_agent function instead of create_react_agent from langgraph.prebuilt. Additionally, the JWT verification refactoring should be hardened by explicitly validating the presence of the iss claim.
| import os | ||
| from typing import Any, Dict, List, Optional | ||
| import httpx | ||
| import httpx2 |
There was a problem hiding this comment.
httpx2 is a real PyPI package (2.13.0) and is what mcp 2.x depends on instead of httpx; the SDK types (http_client=, auth=) must be httpx2 objects (https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md#httpx-and-httpx-sse-replaced-by-httpx2). Not related to HTTP/2. test_python passes on this PR.
| from langchain_openai import ChatOpenAI | ||
| from langchain.agents import AgentExecutor, create_tool_calling_agent | ||
| from langchain_core.prompts import ChatPromptTemplate | ||
| from langchain.agents import create_agent |
There was a problem hiding this comment.
There is no create_agent function in langchain.agents. Based on the usage of agent.astream(..., stream_mode="updates") and the arguments model=llm, tools=lc_tools, you are actually using the LangGraph prebuilt ReAct agent. This requires importing create_react_agent from langgraph.prebuilt.
| from langchain.agents import create_agent | |
| from langgraph.prebuilt import create_react_agent |
There was a problem hiding this comment.
langchain.agents.create_agent exists in LangChain 1.x (https://reference.langchain.com/python/langchain/agents/factory/create_agent) and is the replacement for AgentExecutor; it returns a compiled LangGraph graph, hence astream(..., stream_mode="updates"). langgraph.prebuilt.create_react_agent is the older, now-deprecated path. Verified against langchain 1.4.1.
|
|
||
| # AgentExecutor runs the ReAct/Tool loop automatically! | ||
| agent_executor = AgentExecutor(agent=agent, tools=lc_tools, verbose=True, max_iterations=500) | ||
| agent = create_agent(model=llm, tools=lc_tools) |
There was a problem hiding this comment.
There was a problem hiding this comment.
langchain.agents.create_agent exists in LangChain 1.x (https://reference.langchain.com/python/langchain/agents/factory/create_agent) and is the replacement for AgentExecutor; it returns a compiled LangGraph graph, hence astream(..., stream_mode="updates"). langgraph.prebuilt.create_react_agent is the older, now-deprecated path. Verified against langchain 1.4.1.
| from mcp.server.mcpserver import MCPServer | ||
|
|
||
| mcp = FastMCP("calculator", host="0.0.0.0", port=7777) | ||
| mcp = MCPServer("calculator") |
There was a problem hiding this comment.
There is no MCPServer class or mcp.server.mcpserver module in the MCP Python SDK. The high-level server API is still FastMCP from mcp.server.fastmcp.
| from mcp.server.mcpserver import MCPServer | |
| mcp = FastMCP("calculator", host="0.0.0.0", port=7777) | |
| mcp = MCPServer("calculator") | |
| from mcp.server.fastmcp import FastMCP | |
| mcp = FastMCP("calculator") |
There was a problem hiding this comment.
mcp.server.mcpserver.MCPServer is the mcp 2.x API: 2.0 renamed FastMCP and moved host/port to run() (https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md#fastmcp-renamed-to-mcpserver). On mcp 2.2.0 import mcp.server.fastmcp raises ModuleNotFoundError: ... This is mcp 2.x, where FastMCP was renamed. The Bats e2e on this PR builds and runs this server.
| "mcp>=2.2.0,<3", | ||
| "httpx2>=2.5.0", | ||
| ] |
There was a problem hiding this comment.
There was a problem hiding this comment.
httpx2 is a real PyPI package (2.13.0) and is what mcp 2.x depends on instead of httpx; the SDK types (http_client=, auth=) must be httpx2 objects (https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md#httpx-and-httpx-sse-replaced-by-httpx2). Not related to HTTP/2. test_python passes on this PR.
| from mcp.server.mcpserver import MCPServer | ||
|
|
||
| mcp = FastMCP("greeter", host="0.0.0.0", port=7778) | ||
| mcp = MCPServer("greeter") |
There was a problem hiding this comment.
There is no MCPServer class or mcp.server.mcpserver module in the MCP Python SDK. The high-level server API is still FastMCP from mcp.server.fastmcp.
| from mcp.server.mcpserver import MCPServer | |
| mcp = FastMCP("greeter", host="0.0.0.0", port=7778) | |
| mcp = MCPServer("greeter") | |
| from mcp.server.fastmcp import FastMCP | |
| mcp = FastMCP("greeter") |
There was a problem hiding this comment.
mcp.server.mcpserver.MCPServer is the mcp 2.x API: 2.0 renamed FastMCP and moved host/port to run() (https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md#fastmcp-renamed-to-mcpserver). On mcp 2.2.0 import mcp.server.fastmcp raises ModuleNotFoundError: ... This is mcp 2.x, where FastMCP was renamed. The Bats e2e on this PR builds and runs this server.
| from mcp.server.mcpserver import MCPServer | ||
|
|
||
| mcp = FastMCP("calculator", host="0.0.0.0", port=7777) | ||
| mcp = MCPServer("calculator") |
There was a problem hiding this comment.
There is no MCPServer class or mcp.server.mcpserver module in the MCP Python SDK. The high-level server API is still FastMCP from mcp.server.fastmcp.
| from mcp.server.mcpserver import MCPServer | |
| mcp = FastMCP("calculator", host="0.0.0.0", port=7777) | |
| mcp = MCPServer("calculator") | |
| from mcp.server.fastmcp import FastMCP | |
| mcp = FastMCP("calculator") |
There was a problem hiding this comment.
mcp.server.mcpserver.MCPServer is the mcp 2.x API: 2.0 renamed FastMCP and moved host/port to run() (https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md#fastmcp-renamed-to-mcpserver). On mcp 2.2.0 import mcp.server.fastmcp raises ModuleNotFoundError: ... This is mcp 2.x, where FastMCP was renamed. The Bats e2e on this PR builds and runs this server.
| self._http_client = httpx2.AsyncClient( | ||
| headers=headers, | ||
| follow_redirects=True, | ||
| # The SDK's SSE-friendly defaults; httpx's default 5s read timeout drops the stream. | ||
| timeout=httpx.Timeout(30.0, read=300.0), | ||
| # The SDK's SSE-friendly defaults; the default 5s read timeout drops the stream. | ||
| timeout=httpx2.Timeout(30.0, read=300.0), | ||
| ) |
There was a problem hiding this comment.
Use httpx.AsyncClient with http2=True to enable HTTP/2 support, as httpx2 is not a valid module.
self._http_client = httpx.AsyncClient(
http2=True,
headers=headers,
follow_redirects=True,
# The SDK's SSE-friendly defaults; the default 5s read timeout drops the stream.
timeout=httpx.Timeout(30.0, read=300.0),
)There was a problem hiding this comment.
httpx2 is a real PyPI package (2.13.0) and is what mcp 2.x depends on instead of httpx; the SDK types (http_client=, auth=) must be httpx2 objects (https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md#httpx-and-httpx-sse-replaced-by-httpx2). Not related to HTTP/2. test_python passes on this PR.
| iss, _ := claims["iss"].(string) | ||
| return iss, nil |
There was a problem hiding this comment.
It is safer and more robust to explicitly validate that the iss claim is present and non-empty, returning an error early if it is missing or invalid, rather than returning an empty string which would fail later with a less clear error message.
iss, ok := claims["iss"].(string)
if !ok || iss == "" {
return "", fmt.Errorf("missing or invalid iss claim")
}
return iss, nilThere was a problem hiding this comment.
Done in c8a9d03: issuerHint now returns missing or invalid iss claim, with a test.
The unverified pre-parse was used both to route to the issuer's keys and to check aud, and Verify ran with SkipClientIDCheck, so the audience decision rested entirely on claims that had not been checked yet. It was not exploitable -- the signature covers the same bytes -- but it is the pattern CodeQL's missing-jwt-signature-check flags on oidc.go, and it left the two parsers to agree on how they read a payload. The unverified parse now only yields the issuer hint (and the alg=none refusal); audience and claims are read from the IDToken Verify returns.
Scorecard flags the three Node example images and the pages workflow for installing npm packages without a lockfile: npm install resolves whatever the registry offers at build time. Commit the lockfiles and switch to npm ci, which refuses to run if the lockfile disagrees with package.json and installs the integrity-checked versions recorded there. The site's lockfile was gitignored while the workflow ran an ad-hoc `npm init && npm install autoprefixer@... postcss@...` on top; the manifest now records those same versions exactly, so the built site does not change. The Gemini CLI global install is pinned to a version; a lockfile cannot cover a -g install, so that alert stays open by design.
1ca585f to
82973b7
Compare
Works through the open dependabot PRs and the code-scanning alerts, one commit per concern so each can be reviewed on its own.
Commits
curl: (22) 403; the body that says why was piped intojqand lost. Now printed on any non-2xx. (The 403 itself is operator-side: the service account needs release rights in Play Console, or the app has to be created / first bundle uploaded manually.)calc_server.pystill usedFastMCP(host=, port=); mcp 2.x renamed it toMCPServerand moved transport params torun(). The SDK moves tohttpx2and keeps returning wire-format dicts viaby_alias. Closes build(deps): bump the python-deps group across 4 directories with 14 updates #426.langchain<1pin is what PYSEC-2026-2192/76/2193/2562/77 are against;AgentExecutor→create_agent. Smoke-tested one round against the calc MCP server and a stub OpenAI endpoint.go 1.26.6in root and nano-init,1.25.13in the bridge module. CI and images already run newer; this is the declared floor Scorecard reads.go/missing-jwt-signature-check). The unverified pre-parse now only yields the issuer hint;audand claims come from theIDTokenthatVerifyreturns.npm ci+ lockfiles for the three Node example images and the pages workflow (Scorecard PinnedDependencies add role bindings #73–add streamable discover api #77, Bump github.com/quic-go/quic-go from 0.59.0 to 0.59.1 #91). Site PostCSS versions are recorded exactly as the workflow was installing them, so the built site is unchanged.Not addressed here, and why
e2e-androidfails inkspDebugKotlinwithCan't escape identifier `$Android:appDebug_FunctionComponentRegistry`— theappfunctions-compilerKSP processor does not support Kotlin 2.4 yet. Recommend closing until appfunctions ships a compatible release.pip-compile --generate-hasheslockfiles for every example/e2e image and change how dependabot updates them. Worth doing as its own PR if we want the score.curl | bashinscripts/startup-script.sh(remove wrong statement #83): that is the documented install path for the scale VMs; pinning would mean shipping a release checksum through the VM metadata. Separate discussion.x/crypto/openpgp): transitive via libp2p; the package is not imported anywhere and there is no fixed version.Validation
Local:
go build ./...,go test ./internal/identity ./internal/controlplane ./internal/sambox,make test-python(mcp 2.2.0 / httpx2 2.13.0), calc-mcp image servesinitializeover streamable HTTP,npm cifrom each new lockfile, publish-play.sh exercised against a fake 403. E2E left to CI.