diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index 7dc327a2da7..fd0e5a9da21 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -331,6 +331,11 @@ "test": "%s keeps the full plan in build processes but not final runtime environments (#5896)", "category": "security" }, + { + "file": "test/mcp-tool-discovery-image-contract.test.ts", + "test": "pins reviewed packages and retains the production audit command (#8177)", + "category": "security" + }, { "file": "test/mcp-openshell-workflow.test.ts", "test": "keeps the setup docs aligned with the stable default", diff --git a/docs/security/openclaw-2026.7.1-dependency-review.md b/docs/security/openclaw-2026.7.1-dependency-review.md index d39b67a6457..4b6b8861107 100644 --- a/docs/security/openclaw-2026.7.1-dependency-review.md +++ b/docs/security/openclaw-2026.7.1-dependency-review.md @@ -170,6 +170,13 @@ NemoClaw's focused remediation and locked-install tests, audit validation, registry-signature verification, and provenance verification provide compensating evidence for the reviewed archive. +Undici 8.10.0 forwards plain HTTP targets through a configured HTTP or HTTPS proxy by default. +It sends an HTTP/1.1 absolute-form request target unless `proxyTunnel` is enabled. +Undici 8.5.0 used CONNECT by default for the same target. +NemoClaw's managed image registry requests use HTTPS, so they continue to use CONNECT. +The proxy fixture returns `200` for an ordinary HTTP request and `502` for CONNECT. +It verifies the body, method, host, and full target URL for forwarding. + The OpenClaw core and mcporter graphs resolve `express-rate-limit@8.5.2` and its declared `ip-address@^10.2.0` dependency to affected `10.2.0`. The high-severity `GHSA-mwp4-54f8-5fhr` affects releases through `10.3.0` diff --git a/src/lib/onboard/managed-image-registry-fetch.test.ts b/src/lib/onboard/managed-image-registry-fetch.test.ts index d6399bbd0cb..01f69bb3030 100644 --- a/src/lib/onboard/managed-image-registry-fetch.test.ts +++ b/src/lib/onboard/managed-image-registry-fetch.test.ts @@ -36,11 +36,18 @@ afterEach(async () => { }); describe("managed image registry transport", () => { - it("routes the default registry fetch through the bounded host proxy", async () => { - const tunnels: string[] = []; - const proxy = createServer(); - proxy.on("connect", (request, socket) => { - tunnels.push(request.url ?? ""); + it("forwards a plain HTTP registry fetch through the configured host proxy", async () => { + const requests: Array<{ host: string | undefined; method: string | undefined; url: string }> = + []; + const proxy = createServer((request, response) => { + requests.push({ + host: request.headers.host, + method: request.method, + url: request.url ?? "", + }); + response.end("proxied"); + }); + proxy.on("connect", (_request, socket) => { socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n"); }); const proxyPort = await listen(proxy); @@ -52,8 +59,16 @@ describe("managed image registry transport", () => { }); try { - await expect(session.fetchImpl("http://registry.invalid/v2/")).rejects.toThrow(); - expect(tunnels).toEqual(["registry.invalid:80"]); + const response = await session.fetchImpl("http://registry.invalid/v2/"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("proxied"); + expect(requests).toEqual([ + { + host: "registry.invalid", + method: "GET", + url: "http://registry.invalid/v2/", + }, + ]); } finally { await session.close(); } diff --git a/test/mcp-tool-discovery-image-contract.test.ts b/test/mcp-tool-discovery-image-contract.test.ts index 2df1511c6be..e39510e6b9b 100644 --- a/test/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp-tool-discovery-image-contract.test.ts @@ -14,6 +14,79 @@ const dockerfiles = [ ] as const; describe("MCP tool discovery image contract", () => { + // source-shape-contract: security -- Exact package pins and the production audit command protect the shipped runtime graph + it("pins reviewed packages and retains the production audit command (#8177)", () => { + const packageRoot = path.join(repoRoot, "tools", "mcp-tool-discovery-runtime"); + const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")); + const lock = JSON.parse(fs.readFileSync(path.join(packageRoot, "package-lock.json"), "utf8")); + const review = fs.readFileSync(path.join(packageRoot, "dependency-review.md"), "utf8"); + const installer = fs.readFileSync( + path.join(packageRoot, "install-reviewed-runtime.sh"), + "utf8", + ); + const reviewedSdk = { + name: "@modelcontextprotocol/sdk", + version: "1.30.0", + resolved: "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + integrity: + "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + } as const; + const reviewedPackages = { + "@hono/node-server": { + version: "2.0.12", + resolved: "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + integrity: + "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + }, + "fast-uri": { + version: "3.1.5", + resolved: "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + integrity: + "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + }, + hono: { + version: "4.12.34", + resolved: "https://registry.npmjs.org/hono/-/hono-4.12.34.tgz", + integrity: + "sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==", + }, + "ip-address": { + version: "10.3.1", + resolved: "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + integrity: + "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + }, + } as const; + + expect(manifest.dependencies[reviewedSdk.name]).toBe(reviewedSdk.version); + expect(lock.packages[`node_modules/${reviewedSdk.name}`]).toMatchObject({ + version: reviewedSdk.version, + resolved: reviewedSdk.resolved, + integrity: reviewedSdk.integrity, + }); + expect(review).toContain(`\`${reviewedSdk.name}@${reviewedSdk.version}\``); + expect(review).toContain(`\`${reviewedSdk.integrity}\``); + expect(manifest.overrides).toEqual( + Object.fromEntries( + Object.entries(reviewedPackages).map(([packageName, metadata]) => [ + packageName, + metadata.version, + ]), + ), + ); + for (const [packageName, metadata] of Object.entries(reviewedPackages)) { + expect(lock.packages[`node_modules/${packageName}`]).toMatchObject(metadata); + expect(review).toContain(`\`${packageName}@${metadata.version}\``); + expect(review).toContain(`\`${metadata.integrity}\``); + } + expect( + installer + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.startsWith("npm audit")), + ).toEqual(["npm audit signatures", "npm audit --omit=dev --audit-level=low"]); + }); + it.each( dockerfiles, )("%s installs and probes the bundled runtime at its canonical path (#6901)", (relativePath) => { diff --git a/tools/mcp-tool-discovery-runtime/dependency-review.md b/tools/mcp-tool-discovery-runtime/dependency-review.md index b673e6417f9..8620b0a126f 100644 --- a/tools/mcp-tool-discovery-runtime/dependency-review.md +++ b/tools/mcp-tool-discovery-runtime/dependency-review.md @@ -13,23 +13,57 @@ The shared image runtime uses the official `@modelcontextprotocol/sdk` client so - License: MIT - Locked production graph: `package-lock.json` (lockfile version 3) - Build-only tools: `typescript@6.0.3`, `@types/node@25.5.2`, and `esbuild@0.27.4` (not copied into the final image) -- Security overrides: `@hono/node-server@2.0.11` and `fast-uri@3.1.4` +- Security overrides: + - `@hono/node-server@2.0.12`: `sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==` + - `fast-uri@3.1.5`: `sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==` + - `hono@4.12.34`: `sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==` + - `ip-address@10.3.1`: `sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==` OpenClaw's `mcporter` dependency graph also resolves the official SDK but remains separately locked. This runtime keeps a direct lock because Hermes and LangChain Deep Agents Code must not depend on OpenClaw's adapter package. -The client bundle includes the SDK's AJV validation path, including `ajv-formats` and `fast-uri`, plus `content-type` for standards-compliant response media-type parsing; the `fast-uri` override and `content-type` license are therefore runtime-relevant. It does not include the SDK's Hono server adapter. The build enforces the exact reviewed bundle-package allowlist and emits `BUNDLED_PACKAGES.json` alongside the generated third-party license notice. The exact overrides keep the install and runtime graphs clear of `GHSA-frvp-7c67-39w9` and `GHSA-v2hh-gcrm-f6hx` without changing the SDK client pin. +The client bundle includes the SDK's AJV validation path, including `ajv-formats` and `fast-uri`, plus `content-type` for standards-compliant response media-type parsing. The `fast-uri` override and `content-type` license are therefore runtime-relevant. The bundle does not include the SDK's Hono server adapter or its `hono` and `ip-address` dependencies, but those packages remain part of the installed production graph that the image build audits. The build enforces the exact reviewed bundle-package allowlist and emits `BUNDLED_PACKAGES.json` alongside the generated third-party license notice. The exact overrides keep the installed graph outside the affected ranges for `GHSA-7p8r-x3mc-p8w7`, `GHSA-8j4g-w8fx-2239`, `GHSA-mwp4-54f8-5fhr`, `GHSA-4xrf-jv44-h6hh`, and `GHSA-22jq-vg5j-6vgg` without changing the SDK client pin. + +## 2026-08-03 security refresh + +The NemoClaw `v0.0.100` sandbox image build stopped before creating an image because the committed runtime lock resolved three packages in newly reported advisory ranges. +NemoClaw `main` at `3f3eb6139e089c24397d6a499a10fcde4bdc84da` reproduced the same failure. +The audit boundary worked as designed and remains unchanged. +Issue #8177 records the source, build run, failure receipt, and resume condition. + +Registry metadata binds each audited range: + +- `fast-uri`: `3.1.4` at `6aeece669e4166b2446a89f17c07a3b15dfb7ed4` to `3.1.5` at `5e179cbb4636d5f773ed21126e5bd3068e87e94e`, one patch release +- Hono: `4.12.30` at `b2ae3a2204a48ce15a26448fd746d39745eb1837` to `4.12.34` at `734755ace341607628219ea1dd8ca17f01bf1a5c`, four patch releases +- `ip-address`: `10.2.0` at `80fccaae984618f35dc941efab55cf2440ab37e8` to `10.3.1` at `be7e626c0d49fccb518899f520a3fb64ee189741`, four release increments that cross the `10.3.0` minor boundary + +Each target commit descends from its outgoing commit. The target npm package integrities match the committed lock. + +Concern ledger: + +- `MCP-AUDIT-1` — `fast-uri@3.1.4` accepts malformed or whitespace-smuggled authority introducers that can produce host confusion. Surface: executable AJV format validation in the bundled client. Resolution: pin `fast-uri@3.1.5`, which rejects the ambiguous forms and adds regression tests for them. Validation: exact lock metadata, `npm test`, bundle verification, and the production audit. +- `MCP-AUDIT-2` — `hono@4.12.30` uses a regular expression that can cause excessive work for a large CORS request-header value. Surface: installed SDK server dependency; excluded from the client bundle. Resolution: pin `hono@4.12.34`, which replaces the split expression and adds a regression test for a large request header. Validation: exact lock metadata, bundle-input exclusion, and the production audit. +- `MCP-AUDIT-3` — `ip-address@10.2.0` accepts address forms whose classification can differ across parsers. Surface: installed SDK server dependency; excluded from the client bundle. Resolution: pin `ip-address@10.3.1`, which rejects leading-zero IPv4 octets and stacked subnet suffixes and adds regression tests for IPv4 and IPv6 parsing. Validation: exact lock metadata, bundle-input exclusion, and the production audit. +- `MCP-AUDIT-4` — A dependency-only remediation could weaken the image build's fail-closed audit boundary. Surface: `install-reviewed-runtime.sh`. Resolution: retain `npm audit --omit=dev --audit-level=low` and bind it with the reviewed manifest and lock in the image contract test. Validation: `test/mcp-tool-discovery-image-contract.test.ts`. ## 1.29.0 to 1.30.0 migration review The audited adjacent range contains 10 upstream commits. The published `1.30.0` tag resolves to commit `2d889f2b329e46680ec9bdd565de4616c497825a`, descends from the published `v1.29.0` tag at `e12cbd7078db388152f6e839abdbe09ba01f3f32`, and contains the required client media-type fix at `69749aa5081ddfe675d36da8d96c7e27d83742b8`. The npm publication's `gitHead` matches the target tag, and its registry signature and build provenance verify. -The required client change replaces case-sensitive substring checks with parsed, normalized media types when selecting JSON or SSE response handling. This fixes standards-valid case variants such as `Text/Event-Stream; Charset=UTF-8`. The remaining commits affect SDK server error formatting, server SSE keepalive lifecycle, stdio buffering, upstream tests and workflows, Zod type compatibility, the server-only Hono version range, and the release version. NemoClaw's bundled client does not include the server or stdio implementations, and the committed Hono override remains `2.0.11`. +The required client change replaces case-sensitive substring checks with parsed, normalized media types when selecting JSON or SSE response handling. This fixes standards-valid case variants such as `Text/Event-Stream; Charset=UTF-8`. The remaining commits affect SDK server error formatting, server SSE keepalive lifecycle, stdio buffering, upstream tests and workflows, Zod type compatibility, the server-only Hono version range, and the release version. NemoClaw's bundled client does not include the server or stdio implementations. The committed `@hono/node-server` override is `2.0.12`. Concern ledger: -- `MCP-SDK-130-1` — Client response dispatch rejected case-variant SSE media types. Surface: managed MCP tool discovery initialization and `tools/list`. Resolution: migrate to the official parsed-media-type implementation and cover the full session with a case-variant SSE fixture. Validation: `npm test`. -- `MCP-SDK-130-2` — `content-type@1.0.5` becomes executable bundle input. Surface: response media-type parsing and bundled notices. Resolution: add it to the exact bundle allowlist and verify its MIT text in the generated notice. Validation: `npm run bundle`. -- `MCP-SDK-130-3` — The upstream package widens its Hono server range. Surface: resolved install graph only; the Hono server adapter is absent from the client bundle. Resolution: retain the existing exact `@hono/node-server@2.0.11` security override. Validation: the lock diff and `BUNDLED_PACKAGES.json`. -- `MCP-SDK-130-4` — Other adjacent commits could alter unrelated transports or server behavior. Surface: upstream stdio and server entry points. Resolution: no migration because NemoClaw imports only `client/index.js` and `client/streamableHttp.js`; classify those commits as no runtime impact. Validation: esbuild's exact input graph. +- `MCP-SDK-130-1`: Client response dispatch rejected case-variant SSE media types. Surface: managed MCP tool discovery initialization and `tools/list`. Resolution: migrate to the official parsed-media-type implementation and cover the full session with a case-variant SSE fixture. Validation: `npm test`. +- `MCP-SDK-130-2`: `content-type@1.0.5` becomes executable bundle input. Surface: response media-type parsing and bundled notices. Resolution: add it to the exact bundle allowlist and verify its MIT text in the generated notice. Validation: `npm run bundle`. +- `MCP-SDK-130-3`: The upstream package widens its Hono server range. Surface: resolved install graph only; the Hono server adapter is absent from the client bundle. Resolution: use the reviewed `@hono/node-server@2.0.12` patch release. Validation: the lock diff and `BUNDLED_PACKAGES.json`. +- `MCP-SDK-130-4`: Other adjacent commits could alter unrelated transports or server behavior. Surface: upstream stdio and server entry points. Resolution: no migration because NemoClaw imports only `client/index.js` and `client/streamableHttp.js`; classify those commits as no runtime impact. Validation: esbuild's exact input graph. + +## `@hono/node-server` 2.0.12 review + +Version `2.0.12` is the next patch release and remains within the SDK's declared `^1.19.9 || ^2.0.5` range. It keeps the MIT license, Node.js `>=20` engine, `hono@^4` peer dependency, package exports, and lack of install scripts. + +The `v2.0.11..v2.0.12` source range contains three commits: a test transport replacement, a response-header fix for foreign `Response` objects, and the release commit. The server adapter remains outside NemoClaw's executable client bundle. The annotated tag and release commit are unsigned. During the 2026-08-03 security refresh, `npm audit signatures` verified the exact package's registry signature and Supply-chain Levels for Software Artifacts (SLSA) provenance against release commit `a813b6cdaa15baac3ead84e9e6ed5b72b2353c96`. Upstream Node.js 20, 22, and 24 checks, Windows checks, build checks, and the npm publication check passed. + +The reviewed archive is `https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz` with integrity `sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==`. This patch keeps the fail-closed signature check and adds no exception. ## Build and audit contract @@ -57,6 +91,14 @@ SDK 1.30.0 migration evidence on 2026-07-28: - Exact bundle: 11 packages matching the reviewed allowlist in `BUNDLED_PACKAGES.json`, including `content-type@1.0.5` - `npm run typecheck` and `npm run bundle`: passed +Security refresh evidence on 2026-08-03: + +- `npm ci --ignore-scripts`: installed the exact 98-package lock +- `npm audit signatures`: verified 98 registry signatures and 12 provenance attestations +- `npm test` and `npm run typecheck`: passed +- `npm run bundle`: emitted the same 11-package client bundle with `fast-uri@3.1.5`; `hono` and `ip-address` remain outside the executable bundle +- `npm audit --omit=dev --audit-level=low`: 0 vulnerabilities + ## Updating Regenerate and review the graph explicitly: diff --git a/tools/mcp-tool-discovery-runtime/package-lock.json b/tools/mcp-tool-discovery-runtime/package-lock.json index f5cc6c8aea8..b301d57ddf7 100644 --- a/tools/mcp-tool-discovery-runtime/package-lock.json +++ b/tools/mcp-tool-discovery-runtime/package-lock.json @@ -463,9 +463,9 @@ } }, "node_modules/@hono/node-server": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.11.tgz", - "integrity": "sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "license": "MIT", "engines": { "node": ">=20" @@ -947,9 +947,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -1084,9 +1084,9 @@ } }, "node_modules/hono": { - "version": "4.12.30", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", - "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "version": "4.12.34", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.34.tgz", + "integrity": "sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1135,9 +1135,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/tools/mcp-tool-discovery-runtime/package.json b/tools/mcp-tool-discovery-runtime/package.json index 73aa3261835..3d42f1d468d 100644 --- a/tools/mcp-tool-discovery-runtime/package.json +++ b/tools/mcp-tool-discovery-runtime/package.json @@ -15,8 +15,10 @@ "typescript": "6.0.3" }, "overrides": { - "@hono/node-server": "2.0.11", - "fast-uri": "3.1.4" + "@hono/node-server": "2.0.12", + "fast-uri": "3.1.5", + "hono": "4.12.34", + "ip-address": "10.3.1" }, "engines": { "node": ">=22.19.0"