Skip to content

Security triage: mcp 2.x, LangChain 1.x, Go floor, verified-token audience, npm lockfiles - #432

Merged
aojea merged 6 commits into
google:mainfrom
aojea:security-triage
Sep 18, 2026
Merged

aojea merged 6 commits into
google:mainfrom
aojea:security-triage

Conversation

@aojea

@aojea aojea commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Works through the open dependabot PRs and the code-scanning alerts, one commit per concern so each can be reviewed on its own.

Commits

  1. mobile: surface the Play API error body when publishing fails — the alpha publish run (#35302147393) failed with only curl: (22) 403; the body that says why was piped into jq and 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.)
  2. python: move the MCP servers and SDK to mcp 2.x — takes dependabot build(deps): bump the python-deps group across 4 directories with 14 updates #426's floors. build(deps): bump the python-deps group across 4 directories with 14 updates #426 failed e2e because calc_server.py still used FastMCP(host=, port=); mcp 2.x renamed it to MCPServer and moved transport params to run(). The SDK moves to httpx2 and keeps returning wire-format dicts via by_alias. Closes build(deps): bump the python-deps group across 4 directories with 14 updates #426.
  3. chaos-agent: port to LangChain 1.x — the langchain<1 pin is what PYSEC-2026-2192/76/2193/2562/77 are against; AgentExecutorcreate_agent. Smoke-tested one round against the calc MCP server and a stub OpenAI endpoint.
  4. go: require the toolchain patches for GO-2026-6179/6180go 1.26.6 in root and nano-init, 1.25.13 in the bridge module. CI and images already run newer; this is the declared floor Scorecard reads.
  5. identity: decide the audience on the verified token — CodeQL alert add e2e tests #22 (go/missing-jwt-signature-check). The unverified pre-parse now only yields the issuer hint; aud and claims come from the IDToken that Verify returns.
  6. build: install npm dependencies from committed lockfilesnpm ci + lockfiles for the three Node example images and the pages workflow (Scorecard PinnedDependencies add role bindings #73add 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

  • build(deps): bump org.jetbrains.kotlin.android from 2.3.20 to 2.4.20 in /mobile/sam-node-app/android #421 (kotlin.android 2.3.20 → 2.4.20): its e2e-android fails in kspDebugKotlin with Can't escape identifier `$Android:appDebug_FunctionComponentRegistry` — the appfunctions-compiler KSP processor does not support Kotlin 2.4 yet. Recommend closing until appfunctions ships a compatible release.
  • pip hash pinning (User dashboard #70update docs #72, update mcp-client tool #78Fix hub authentication #81): would need pip-compile --generate-hashes lockfiles for every example/e2e image and change how dependabot updates them. Worth doing as its own PR if we want the score.
  • curl | bash in scripts/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.
  • GO-2024-3218 (kad-dht): already on the govulncheck accepted list; no upstream fix exists.
  • GO-2026-5932 (x/crypto/openpgp): transitive via libp2p; the package is not imported anywhere and there is no fixed version.
  • Scorecard process alerts (CII badge, code review, fuzzing, SAST coverage): not code-fixable.
  • KGP warnings from firebase_ plugins:* informational; upstream plugin migration to built-in Kotlin.

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 serves initialize over streamable HTTP, npm ci from each new lockfile, publish-play.sh exercised against a fake 403. E2E left to CI.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Import httpx instead of the non-existent httpx2 module.

Suggested change
import httpx2
import httpx

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/chaos-agent/agent.py
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
from langchain.agents import create_agent
from langgraph.prebuilt import create_react_agent

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/chaos-agent/agent.py

# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Use create_react_agent from langgraph.prebuilt to correctly initialize the LangGraph ReAct agent, as create_agent does not exist in LangChain.

Suggested change
agent = create_agent(model=llm, tools=lc_tools)
agent = create_react_agent(model=llm, tools=lc_tools)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +2 to +4
from mcp.server.mcpserver import MCPServer

mcp = FastMCP("calculator", host="0.0.0.0", port=7777)
mcp = MCPServer("calculator")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +12 to 14
"mcp>=2.2.0,<3",
"httpx2>=2.5.0",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is no httpx2 package on PyPI. The standard library and MCP SDK use httpx. To enable HTTP/2 support, you should use the httpx[http2] extra with a valid version (e.g., >=0.27.0).

Suggested change
"mcp>=2.2.0,<3",
"httpx2>=2.5.0",
]
"mcp>=2.2.0,<3",
"httpx[http2]>=0.27.0",
]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +2 to +4
from mcp.server.mcpserver import MCPServer

mcp = FastMCP("greeter", host="0.0.0.0", port=7778)
mcp = MCPServer("greeter")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +2 to +4
from mcp.server.mcpserver import MCPServer

mcp = FastMCP("calculator", host="0.0.0.0", port=7777)
mcp = MCPServer("calculator")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Suggested change
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")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +38 to 43
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),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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),
        )

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/identity/oidc.go Outdated
Comment on lines +84 to +85
iss, _ := claims["iss"].(string)
return iss, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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, nil

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@aojea
aojea merged commit 230e95c into google:main Sep 18, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant