From e049cb6880b745185c27a3048f8a58ca95fc73e8 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Fri, 10 Jul 2026 18:59:03 +0800 Subject: [PATCH] fix: clear dependency security advisories --- .gitea/workflows/checker-check.yml | 6 + .gitea/workflows/hub-check.yml | 3 + ...21-clear-dependency-security-advisories.md | 35 ++++- .scratch/saas-production-readiness/map.md | 1 + Cargo.lock | 32 ++--- Cargo.toml | 5 + hub/deploy/README.md | 5 + hub/deploy/deploy_platform.sh | 4 +- hub/package-lock.json | 59 ++++++--- hub/package.json | 8 +- hub/test/integration/lark-sdk-axios.test.ts | 125 ++++++++++++++++++ 11 files changed, 242 insertions(+), 41 deletions(-) create mode 100644 hub/test/integration/lark-sdk-axios.test.ts diff --git a/.gitea/workflows/checker-check.yml b/.gitea/workflows/checker-check.yml index 89d5bad..96e027d 100644 --- a/.gitea/workflows/checker-check.yml +++ b/.gitea/workflows/checker-check.yml @@ -37,6 +37,12 @@ jobs: restore-keys: | cargo-${{ runner.os }}- + - name: Install cargo-audit 0.22.2 + run: cargo install cargo-audit --version 0.22.2 --locked + + - name: Audit Rust vulnerabilities and unsoundness + run: cargo audit --deny unsound + # Fonts for typst compilation (WU-4 onward): the engineering files are # CJK-heavy, so the compile-check / build tests need a CJK font present. # Harmless before those crates exist. diff --git a/.gitea/workflows/hub-check.yml b/.gitea/workflows/hub-check.yml index 5216d10..0ef40ca 100644 --- a/.gitea/workflows/hub-check.yml +++ b/.gitea/workflows/hub-check.yml @@ -43,6 +43,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Audit production Node dependencies + run: npm run audit:production + - name: Install Linux sandbox dependency run: sudo apt-get update && sudo apt-get install --yes bubblewrap socat diff --git a/.scratch/saas-production-readiness/issues/21-clear-dependency-security-advisories.md b/.scratch/saas-production-readiness/issues/21-clear-dependency-security-advisories.md index b088893..1a4704e 100644 --- a/.scratch/saas-production-readiness/issues/21-clear-dependency-security-advisories.md +++ b/.scratch/saas-production-readiness/issues/21-clear-dependency-security-advisories.md @@ -1,7 +1,7 @@ # Clear dependency advisories and gate releases Type: task -Status: open +Status: resolved ## Question @@ -9,3 +9,36 @@ Remove every current `npm audit --omit=dev` high finding and every `cargo audit` vulnerability/unsound finding through tested upgrades, compatible overrides or upstream fixes—not ignores—and add reproducible Node and Rust security-audit gates to CI and the production release evidence. + +## Answer + +All current production-high Node advisories and all Rust vulnerability/unsound +advisories are removed without advisory ignores: + +- `@larksuiteoapi/node-sdk` 1.70.0 declares Axios `~1.13.3`, which prevented npm + from selecting a patched release. The Hub now has a targeted npm override to + Axios 1.18.1. The direct SDK stays current; the override is locked and the + full Hub type/build/test gate validates the resolved tree. A deterministic + local-HTTP smoke also drives the real SDK/Axios stack through tenant-token, + authenticated success and HTTP-error paths without external credentials. +- `memmap2` is upgraded to 0.9.11 and `crossbeam-epoch` to 0.9.20. +- `plist` is upgraded to 1.10.0, which moves its XML parser to `quick-xml` + 0.41.0. +- crates.io `citationberg` 0.7.0 still pins vulnerable `quick-xml` 0.38.4. + Cargo now patches it to the exact upstream citationberg commit that adopts + `quick-xml` 0.41.0; the git revision is immutable in both manifest and lock. + +`hub-check` runs the checked-in `npm run audit:production` command immediately +after `npm ci`. `checker-check` installs the pinned cargo-audit 0.22.2 release +with `--locked`, then runs `cargo audit --deny unsound`; vulnerabilities fail by +default and unsound warnings are promoted to failures. + +Current evidence: + +- `npm audit --omit=dev --audit-level=high`: 0 vulnerabilities. +- `cargo audit --deny unsound`: 0 vulnerabilities and 0 unsound warnings. +- Four Rust `unmaintained` informational warnings remain visible and are not + ignored; they are outside this issue's vulnerability/unsound acceptance + criterion and will continue to appear in CI output. +- Hub: type-check, build and 280 tests pass (10 environment-specific skips). +- Rust: fmt, clippy with warnings denied, and all 53 workspace tests pass. diff --git a/.scratch/saas-production-readiness/map.md b/.scratch/saas-production-readiness/map.md index 3a8a745..b93b4d8 100644 --- a/.scratch/saas-production-readiness/map.md +++ b/.scratch/saas-production-readiness/map.md @@ -48,6 +48,7 @@ rollback and recovery, and no known critical security or data-integrity gaps. - [Confine the agent runtime and protect service credentials](issues/16-confine-agent-and-protect-credentials.md) — replace the Agent environment, deny host reads/writes from root, keep every writable SDK path inside the canonical project, require bubblewrap plus socat, emit bounded correlated diagnostics, and prove the boundary with the real Linux SDK under a capability-free non-root identity. - [Close MCP context and file-delivery escape paths](issues/17-close-mcp-data-egress-escapes.md) — use no-follow project-rooted file operations and delivery snapshots, remove Git-root delivery, and bind every Feishu MCP read to the run's project and chat. - [Enforce Organization status at the shared authorization seam](issues/22-enforce-organization-status.md) — deny non-ACTIVE tenants before principal resolution, serialize every customer mutation and run admission against Organization lifecycle updates, reauthorize queued work, and stage/publish attachments with crash recovery and explicit filesystem compensation. +- [Clear dependency advisories and gate releases](issues/21-clear-dependency-security-advisories.md) — resolve Node production-high and Rust vulnerability/unsound advisories with locked compatible upgrades plus an exact upstream citationberg fix, and make both audits mandatory CI gates without ignores. ## Fog diff --git a/Cargo.lock b/Cargo.lock index 530e812..7a97ef7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,10 +264,9 @@ dependencies = [ [[package]] name = "citationberg" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756ff1e3d43a9ecc8183932fb4d9fd3971236f3ce4acb62fe51d1cd43297547d" +source = "git+https://github.com/typst/citationberg?rev=06a591e2f237d25e1dfdedac3f3d1494c496c52d#06a591e2f237d25e1dfdedac3f3d1494c496c52d" dependencies = [ - "quick-xml 0.38.4", + "quick-xml", "serde", "serde_path_to_error", ] @@ -481,9 +480,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -1403,9 +1402,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -1642,13 +1641,13 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64", "indexmap", - "quick-xml 0.39.4", + "quick-xml", "serde", "time", ] @@ -1756,23 +1755,14 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", "serde", ] -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.45" diff --git a/Cargo.toml b/Cargo.toml index fa404a9..6f9d2d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,8 @@ serde = { version = "1", features = ["derive"] } toml = "0.8" cph-diag = { path = "crates/cph-diag" } cph-model = { path = "crates/cph-model" } + +# citationberg 0.7.0 on crates.io pins vulnerable quick-xml 0.38. Upstream has +# adopted quick-xml 0.41 but has not published a replacement release yet. +[patch.crates-io] +citationberg = { git = "https://github.com/typst/citationberg", rev = "06a591e2f237d25e1dfdedac3f3d1494c496c52d" } diff --git a/hub/deploy/README.md b/hub/deploy/README.md index 337267e..00e5c26 100644 --- a/hub/deploy/README.md +++ b/hub/deploy/README.md @@ -21,6 +21,11 @@ run the same command again. No systemd unit is installed until configuration, paths, Node, bubblewrap, `socat`, `cph --version`, PostgreSQL connectivity and directory ownership all pass preflight. +The deployment path also runs `npm run audit:production` from the locked clean +install before building or restarting the service. A current high or critical +production advisory therefore blocks deployment instead of becoming a warning +buried in CI output. + An existing service account is accepted only when its primary group, home and non-login shell match the requested configuration and it has no supplementary groups. After provisioning, the installer executes path-access, built Hub, diff --git a/hub/deploy/deploy_platform.sh b/hub/deploy/deploy_platform.sh index 7c79cd2..86ba95f 100755 --- a/hub/deploy/deploy_platform.sh +++ b/hub/deploy/deploy_platform.sh @@ -36,8 +36,8 @@ rsync -az --delete \ -e "ssh ${SSH_OPTS[*]}" \ "$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/" -# 2. Install deps + build on the host. -ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "cd '$HUB_DIR' && npm ci && npm run build" +# 2. Install deps, prove the production dependency tree, then build on the host. +ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" "cd '$HUB_DIR' && npm ci && npm run audit:production && npm run build" # 3. Ensure the service is installed (idempotent), then restart. ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" " diff --git a/hub/package-lock.json b/hub/package-lock.json index effaf97..f5f8104 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", - "@larksuiteoapi/node-sdk": "^1.66.1", + "@larksuiteoapi/node-sdk": "^1.70.0", "@prisma/client": "^6.19.3", "ai": "^7.0.16", "dotenv": "^17.4.2", @@ -874,9 +874,9 @@ "license": "MIT" }, "node_modules/@larksuiteoapi/node-sdk": { - "version": "1.68.0", - "resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.68.0.tgz", - "integrity": "sha512-ip14+pWAv2La3bss4oDDtee2P5xBLDXuvmzhTvkxQMcPA5jCffFsler1TvNt4MyW6pexscj72AEWektyZwU9Yw==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.70.0.tgz", + "integrity": "sha512-CzVfMbh2MO+cVWQgQGeYN9KkFdX6mofYYTsIjImU0hQqlxaXKzHcUEwG6mrpsmU2LWqYYtIGwCSIZjYQRjD6kw==", "license": "MIT", "dependencies": { "axios": "~1.13.3", @@ -1621,6 +1621,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ai": { "version": "7.0.16", "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.16.tgz", @@ -1717,14 +1729,15 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/body-parser": { @@ -2004,7 +2017,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2823,6 +2835,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -3353,8 +3378,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.15", @@ -3735,10 +3759,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pure-rand": { "version": "6.1.0", diff --git a/hub/package.json b/hub/package.json index e2103ce..75d0f9f 100644 --- a/hub/package.json +++ b/hub/package.json @@ -9,7 +9,7 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", - "@larksuiteoapi/node-sdk": "^1.66.1", + "@larksuiteoapi/node-sdk": "^1.70.0", "@prisma/client": "^6.19.3", "ai": "^7.0.16", "dotenv": "^17.4.2", @@ -22,12 +22,18 @@ "typescript": "^5.7.0", "vitest": "^4.1.10" }, + "overrides": { + "@larksuiteoapi/node-sdk": { + "axios": "1.18.1" + } + }, "description": "Curriculum Project Hub — Feishu-group collaboration + Claude Code SDK agent runtime. Aligns to spec/System (ADR-0001..0004, 0017).", "scripts": { "dev": "npm run prisma:migrate && tsx watch src/server.ts", "build": "tsc -p tsconfig.json", "start": "npm run prisma:migrate && node dist/server.js", "check": "tsc -p tsconfig.json --noEmit", + "audit:production": "npm audit --omit=dev --audit-level=high", "prisma:generate": "prisma generate --schema prisma/schema.prisma", "prisma:validate": "DATABASE_URL=${DATABASE_URL:-postgresql://stub:stub@127.0.0.1:5432/stub} prisma validate --schema prisma/schema.prisma", "prisma:migrate": "DATABASE_URL=${DATABASE_URL:-postgresql://paradigm:paradigm@127.0.0.1:5432/paradigm} prisma migrate deploy --schema prisma/schema.prisma", diff --git a/hub/test/integration/lark-sdk-axios.test.ts b/hub/test/integration/lark-sdk-axios.test.ts new file mode 100644 index 0000000..576f449 --- /dev/null +++ b/hub/test/integration/lark-sdk-axios.test.ts @@ -0,0 +1,125 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import * as lark from "@larksuiteoapi/node-sdk"; +import { afterEach, describe, expect, it } from "vitest"; + +const openServers: ReturnType[] = []; + +describe("real Lark SDK HTTP transport", () => { + afterEach(async () => { + await Promise.all(openServers.splice(0).map(async (server) => { + server.closeAllConnections(); + await new Promise((resolve, reject) => { + server.close((error) => error === undefined ? resolve() : reject(error)); + }); + })); + }); + + it("uses overridden Axios for token, authenticated success, and HTTP error paths", async () => { + const requests: Array<{ + readonly method: string | undefined; + readonly url: string | undefined; + readonly authorization: string | undefined; + readonly body: string; + }> = []; + const server = createServer(async (request, response) => { + const body = await readBody(request); + requests.push({ + method: request.method, + url: request.url, + authorization: request.headers.authorization, + body, + }); + routeSmokeRequest(request, response, body); + }); + openServers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (address === null || typeof address === "string") throw new Error("local HTTP server has no TCP address"); + const domain = `http://127.0.0.1:${address.port}`; + const client = new lark.Client({ + appId: "cli_transport_smoke", + appSecret: "transport-smoke-secret", + domain, + loggerLevel: lark.LoggerLevel.error, + }); + + await expect(client.request<{ readonly code: number; readonly data: { readonly ok: boolean } }>({ + method: "GET", + url: "/open-apis/smoke/success", + params: { probe: "axios-override" }, + })).resolves.toEqual({ code: 0, data: { ok: true } }); + + await expect(client.request({ + method: "GET", + url: "/open-apis/smoke/failure", + })).rejects.toMatchObject({ + response: { + status: 429, + data: { code: 999_429, msg: "forced smoke failure" }, + }, + }); + + expect(requests).toHaveLength(3); + expect(requests[0]).toMatchObject({ + method: "POST", + url: "/open-apis/auth/v3/tenant_access_token/internal", + authorization: undefined, + }); + expect(JSON.parse(requests[0]!.body)).toEqual({ + app_id: "cli_transport_smoke", + app_secret: "transport-smoke-secret", + }); + expect(requests[1]).toMatchObject({ + method: "GET", + url: "/open-apis/smoke/success?probe=axios-override", + authorization: "Bearer tenant-transport-smoke", + }); + expect(requests[2]).toMatchObject({ + method: "GET", + url: "/open-apis/smoke/failure", + authorization: "Bearer tenant-transport-smoke", + }); + }); +}); + +async function readBody(request: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks).toString("utf8"); +} + +function routeSmokeRequest(request: IncomingMessage, response: ServerResponse, body: string): void { + if (request.method === "POST" && request.url === "/open-apis/auth/v3/tenant_access_token/internal") { + const credential = JSON.parse(body) as { app_id?: unknown; app_secret?: unknown }; + if (credential.app_id !== "cli_transport_smoke" || credential.app_secret !== "transport-smoke-secret") { + sendJson(response, 401, { code: 401, msg: "bad smoke credential" }); + return; + } + sendJson(response, 200, { + code: 0, + msg: "ok", + tenant_access_token: "tenant-transport-smoke", + expire: 3_600, + }); + return; + } + if (request.method === "GET" && request.url === "/open-apis/smoke/success?probe=axios-override") { + sendJson(response, 200, { code: 0, data: { ok: true } }); + return; + } + if (request.method === "GET" && request.url === "/open-apis/smoke/failure") { + sendJson(response, 429, { code: 999_429, msg: "forced smoke failure" }); + return; + } + sendJson(response, 404, { code: 404, msg: "unknown smoke route" }); +} + +function sendJson(response: ServerResponse, statusCode: number, body: unknown): void { + response.writeHead(statusCode, { "content-type": "application/json" }); + response.end(JSON.stringify(body)); +}