fix: clear dependency security advisories

This commit is contained in:
2026-07-10 18:59:03 +08:00
parent d730e51c3d
commit e049cb6880
11 changed files with 242 additions and 41 deletions
+6
View File
@@ -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.
+3
View File
@@ -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
@@ -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.
@@ -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
Generated
+11 -21
View File
@@ -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"
+5
View File
@@ -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" }
+5
View File
@@ -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,
+2 -2
View File
@@ -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" "
+43 -16
View File
@@ -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",
+7 -1
View File
@@ -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",
+125
View File
@@ -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<typeof createServer>[] = [];
describe("real Lark SDK HTTP transport", () => {
afterEach(async () => {
await Promise.all(openServers.splice(0).map(async (server) => {
server.closeAllConnections();
await new Promise<void>((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<void>((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<string> {
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));
}