From 4d3208a642681e39fd9bfe75572869f1de1c8771 Mon Sep 17 00:00:00 2001 From: souta <109493063+souta-lab@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:46:54 +0900 Subject: [PATCH 1/4] fix(coding-agent): update vulnerable HTTP and glob dependencies --- package-lock.json | 33 +++++++++++-- .../install-lock/package-lock.json | 47 +++++++++++-------- packages/coding-agent/package.json | 4 +- .../fixtures/excessive-content-encoding.mjs | 34 ++++++++++++++ .../coding-agent/test/http-dispatcher.test.ts | 12 +++++ 5 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 packages/coding-agent/test/fixtures/excessive-content-encoding.mjs diff --git a/package-lock.json b/package-lock.json index bad8cb209..3b1bacd06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2276,6 +2276,12 @@ "node": ">=18.0.0" } }, + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", + "license": "BlueOak-1.0.0" + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -8275,6 +8281,7 @@ "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.8" @@ -10502,9 +10509,9 @@ } }, "node_modules/undici": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", - "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", + "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -11441,12 +11448,12 @@ "hosted-git-info": "9.0.3", "ignore": "7.0.8", "jiti": "2.7.0", - "minimatch": "10.2.6", + "minimatch": "11.0.1", "proper-lockfile": "4.1.2", "quickjs-wasi": "3.6.2", "semver": "7.8.5", "typebox": "1.3.27", - "undici": "8.10.2", + "undici": "8.12.2", "yaml": "2.9.0" }, "bin": { @@ -11545,6 +11552,22 @@ "extraneous": true, "license": "MIT" }, + "packages/coding-agent/node_modules/minimatch": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.1.tgz", + "integrity": "sha512-t+Vn6DX4wPCxS23y03C0uhF7S1jpi1GfqPrnlEipCJaA2ykiNbMkVlDutZtmvLfRcUN6R8AYVlzTsQrT4RZoLQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/nonrecursive-glob-match": "^1.0.1", + "brace-expansion": "^5.0.12" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/durable": { "name": "@hasilan-code/pi-durable", "version": "1.4.0", diff --git a/packages/coding-agent/install-lock/package-lock.json b/packages/coding-agent/install-lock/package-lock.json index aab7d7a78..a1a804b19 100644 --- a/packages/coding-agent/install-lock/package-lock.json +++ b/packages/coding-agent/install-lock/package-lock.json @@ -535,12 +535,12 @@ "hosted-git-info": "9.0.3", "ignore": "7.0.8", "jiti": "2.7.0", - "minimatch": "10.2.6", + "minimatch": "11.0.1", "proper-lockfile": "4.1.2", "quickjs-wasi": "3.6.2", "semver": "7.8.5", "typebox": "1.3.27", - "undici": "8.10.2", + "undici": "8.12.2", "yaml": "2.9.0" }, "bin": { @@ -550,6 +550,22 @@ "node": ">=22.19.0" } }, + "node_modules/@hasilan-code/pi-coding-agent/node_modules/minimatch": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.1.tgz", + "integrity": "sha512-t+Vn6DX4wPCxS23y03C0uhF7S1jpi1GfqPrnlEipCJaA2ykiNbMkVlDutZtmvLfRcUN6R8AYVlzTsQrT4RZoLQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/nonrecursive-glob-match": "^1.0.1", + "brace-expansion": "^5.0.12" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@hasilan-code/pi-mcp": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@hasilan-code/pi-mcp/-/pi-mcp-1.4.0.tgz", @@ -582,6 +598,12 @@ "node": ">=22.19.0" } }, + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", + "license": "BlueOak-1.0.0" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1173,21 +1195,6 @@ "node": ">=18" } }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1429,9 +1436,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", - "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", + "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 20d22ec39..dcc6ceb73 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -56,12 +56,12 @@ "hosted-git-info": "9.0.3", "ignore": "7.0.8", "jiti": "2.7.0", - "minimatch": "10.2.6", + "minimatch": "11.0.1", "proper-lockfile": "4.1.2", "quickjs-wasi": "3.6.2", "semver": "7.8.5", "typebox": "1.3.27", - "undici": "8.10.2", + "undici": "8.12.2", "yaml": "2.9.0" }, "overrides": { diff --git a/packages/coding-agent/test/fixtures/excessive-content-encoding.mjs b/packages/coding-agent/test/fixtures/excessive-content-encoding.mjs new file mode 100644 index 000000000..f9d9c0fae --- /dev/null +++ b/packages/coding-agent/test/fixtures/excessive-content-encoding.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:net"; +import { setTimeout } from "node:timers/promises"; +import * as undici from "undici"; +import { configureHttpDispatcher } from "../../src/core/http-dispatcher.ts"; + +// Run in a child process: the regression escapes fetch's rejection handler. +for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy"]) { + delete process.env[key]; +} +if (process.argv[2] === "configured-global") configureHttpDispatcher(); +const fetcher = process.argv[2] === "configured-global" ? globalThis.fetch : undici.fetch; +const server = createServer((socket) => { + socket.on("error", () => {}); + socket.once("data", () => { + socket.end( + "HTTP/1.1 200 OK\r\n" + + "Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip\r\n" + + "Content-Length: 100\r\nConnection: close\r\n\r\n", + ); + }); +}); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +try { + const address = server.address(); + assert(address && typeof address !== "string"); + await assert.rejects(fetcher(`http://127.0.0.1:${address.port}/`), /fetch failed/); + // Give the prematurely closed body time to emit its asynchronous error. + await setTimeout(100); +} finally { + await undici.getGlobalDispatcher().close(); + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); +} +console.log("survived excessive content encodings"); diff --git a/packages/coding-agent/test/http-dispatcher.test.ts b/packages/coding-agent/test/http-dispatcher.test.ts index 74784e169..de64b7b16 100644 --- a/packages/coding-agent/test/http-dispatcher.test.ts +++ b/packages/coding-agent/test/http-dispatcher.test.ts @@ -1,6 +1,8 @@ +import { execFileSync } from "node:child_process"; import http from "node:http"; import net from "node:net"; import tls from "node:tls"; +import { fileURLToPath } from "node:url"; import * as undici from "undici"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { applyHttpProxySettings, configureHttpDispatcher } from "../src/core/http-dispatcher.ts"; @@ -62,6 +64,16 @@ describe("http dispatcher", () => { const originalFetch = globalThis.fetch; let savedProxyEnv: Record<(typeof DISPATCHER_PROXY_ENV_KEYS)[number], string | undefined>; + // Regression for seipass/hasilan-code#1 (GHSA-xjj6-f33q-929c). + it.each(["fetch", "configured-global"])("survives excessive content encodings with %s", (mode) => { + const output = execFileSync( + process.execPath, + [fileURLToPath(new URL("./fixtures/excessive-content-encoding.mjs", import.meta.url)), mode], + { encoding: "utf8", timeout: 10_000 }, + ); + expect(output.trim()).toBe("survived excessive content encodings"); + }); + beforeEach(() => { savedProxyEnv = Object.fromEntries(DISPATCHER_PROXY_ENV_KEYS.map((key) => [key, process.env[key]])) as Record< (typeof DISPATCHER_PROXY_ENV_KEYS)[number], From 1a47b2a55de85eb00d3a84707ba97887d98cf2ed Mon Sep 17 00:00:00 2001 From: souta <109493063+souta-lab@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:48:47 +0900 Subject: [PATCH 2/4] fix: run HTTP security regressions in Hasilan CI --- scripts/test-hasilan.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test-hasilan.mjs b/scripts/test-hasilan.mjs index 3df50ee15..787505307 100644 --- a/scripts/test-hasilan.mjs +++ b/scripts/test-hasilan.mjs @@ -48,6 +48,7 @@ execFileSync( vitest, "--run", "--maxWorkers=2", + "test/http-dispatcher.test.ts", "test/suite/agent-session-context-injection.test.ts", "test/suite/agent-session-queue.test.ts", ], From d1ac1ee5d35c682d62d658b4eeca8ba2b7ac85b5 Mon Sep 17 00:00:00 2001 From: seipass Date: Sun, 1 Nov 2026 23:13:43 +0900 Subject: [PATCH 3/4] fix: configure patched HTTP client in Hasilan host --- packages/coding-agent/src/index.ts | 1 + packages/web-harness/src/server/main.ts | 11 ++++- .../test/fixtures/http-dispatcher-startup.mjs | 43 +++++++++++++++++++ .../test/http-dispatcher-startup.test.ts | 14 ++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 packages/web-harness/test/fixtures/http-dispatcher-startup.mjs create mode 100644 packages/web-harness/test/http-dispatcher-startup.test.ts diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 578a0b492..fa9abbd9b 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -218,6 +218,7 @@ export { } from "./core/extensions/index.ts"; // Footer data provider (git branch + extension statuses - data not otherwise available to extensions) export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts"; +export { applyHttpProxySettings, configureHttpDispatcher } from "./core/http-dispatcher.ts"; export type { RegisteredMcpServer } from "./core/mcp-servers.ts"; export { convertToLlm } from "./core/messages.ts"; export { ModelRegistry } from "./core/model-registry.ts"; diff --git a/packages/web-harness/src/server/main.ts b/packages/web-harness/src/server/main.ts index 89d975396..ff08d7e36 100644 --- a/packages/web-harness/src/server/main.ts +++ b/packages/web-harness/src/server/main.ts @@ -1,7 +1,12 @@ import { randomBytes } from "node:crypto"; import { join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { ModelRuntime, type ToolDefinition } from "@hasilan-code/pi-coding-agent"; +import { + applyHttpProxySettings, + configureHttpDispatcher, + ModelRuntime, + type ToolDefinition, +} from "@hasilan-code/pi-coding-agent"; import { toString as qrCodeText } from "qrcode"; import { ATTACHMENT_LIMITS } from "../shared/attachments.ts"; import { CANVAS_LIMITS, canvasSaveSchema, isCanvasDocument, serializeCanvasDesign } from "../shared/canvas.ts"; @@ -79,6 +84,9 @@ export async function startHarness(options: CliOptions, hooks: HarnessHooks = {} async function createHarness(options: CliOptions, hooks: HarnessHooks) { const cwd = await resolveCliWorkspace(options); const agentDir = getHarnessAgentDir(); + const agentSettings = createHarnessSettings(cwd, agentDir); + applyHttpProxySettings(agentSettings.getGlobalSettings().httpProxy); + configureHttpDispatcher(agentSettings.getHttpIdleTimeoutMs()); const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json"), @@ -86,7 +94,6 @@ async function createHarness(options: CliOptions, hooks: HarnessHooks) { }); const settings = await SettingsService.open(agentDir, modelRuntime); const uiPreferences = await UiPreferencesStore.open(agentDir); - const agentSettings = createHarnessSettings(cwd, agentDir); const authLogin = new AuthLoginManager(modelRuntime, { getDeviceId: () => agentSettings.getOrCreateDeviceId() }); const artifacts = new ArtifactStore(); const browser = new BrowserManager(artifacts); diff --git a/packages/web-harness/test/fixtures/http-dispatcher-startup.mjs b/packages/web-harness/test/fixtures/http-dispatcher-startup.mjs new file mode 100644 index 000000000..63149f9a3 --- /dev/null +++ b/packages/web-harness/test/fixtures/http-dispatcher-startup.mjs @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { mkdtemp, rm } from "node:fs/promises"; +import { createServer } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout } from "node:timers/promises"; +import * as undici from "undici"; +import { startHarness } from "../../src/server/main.ts"; +import { WidgetData } from "../../src/server/widget-data.ts"; + +const root = await mkdtemp(join(tmpdir(), "hasilan-http-startup-")); +process.env.HASILAN_CODE_AGENT_DIR = join(root, "agent"); +process.env.PI_OFFLINE = "1"; +for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy"]) { + delete process.env[key]; +} +let harness; +const server = createServer((socket) => { + socket.on("error", () => {}); + socket.once("data", () => { + socket.end( + "HTTP/1.1 200 OK\r\n" + + "Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip\r\n" + + "Content-Length: 100\r\nConnection: close\r\n\r\n", + ); + }); +}); +try { + harness = await startHarness({ cwd: root, port: 0, noOpen: true, help: false, lan: false, development: false }); + assert.equal(globalThis.fetch, undici.fetch, "Hasilan must use the patched undici fetch"); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + assert(address && typeof address !== "string"); + const widgets = new WidgetData(); + await assert.rejects(widgets.read(new URL(`http://127.0.0.1:${address.port}/`), 0, (value) => value)); + await setTimeout(100); +} finally { + await harness?.shutdown(); + if (server.listening) await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + await undici.getGlobalDispatcher().close(); + await rm(root, { recursive: true, force: true }); +} +console.log("survived excessive content encodings"); diff --git a/packages/web-harness/test/http-dispatcher-startup.test.ts b/packages/web-harness/test/http-dispatcher-startup.test.ts new file mode 100644 index 000000000..c8a7974d6 --- /dev/null +++ b/packages/web-harness/test/http-dispatcher-startup.test.ts @@ -0,0 +1,14 @@ +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +// Regression for seipass/hasilan-code#1 (GHSA-xjj6-f33q-929c). +it("uses the patched HTTP implementation after the shared web and desktop startup", () => { + const resolver = new URL("../../coding-agent/src/experimental/source-resolver.ts", import.meta.url).href; + const fixture = fileURLToPath(new URL("./fixtures/http-dispatcher-startup.mjs", import.meta.url)); + const output = execFileSync(process.execPath, ["--import", resolver, fixture], { + encoding: "utf8", + timeout: 30_000, + }); + expect(output.trim()).toBe("survived excessive content encodings"); +}); From ae1834524efd1ba2b43ff8de159f00b7e8ce5464 Mon Sep 17 00:00:00 2001 From: seipass Date: Sun, 1 Nov 2026 23:19:15 +0900 Subject: [PATCH 4/4] fix: allow time for harness startup regression test --- packages/web-harness/test/http-dispatcher-startup.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-harness/test/http-dispatcher-startup.test.ts b/packages/web-harness/test/http-dispatcher-startup.test.ts index c8a7974d6..557adef37 100644 --- a/packages/web-harness/test/http-dispatcher-startup.test.ts +++ b/packages/web-harness/test/http-dispatcher-startup.test.ts @@ -11,4 +11,4 @@ it("uses the patched HTTP implementation after the shared web and desktop startu timeout: 30_000, }); expect(output.trim()).toBe("survived excessive content encodings"); -}); +}, 40_000);