From 7e0e22f6e2e63b15287c4bc5fc91b406b37801b2 Mon Sep 17 00:00:00 2001 From: Mel0ny <72508862+mel0nyrame@users.noreply.github.com> Date: Fri, 7 Aug 2026 03:59:08 +0800 Subject: [PATCH] fix(deps): upgrade adm-zip to 0.6.0 to fix GHSA-xcpc-8h2w-3j85 (#2643) adm-zip <0.6.0 allows a crafted ZIP to trigger a ~4GB memory allocation (GHSA-xcpc-8h2w-3j85). Bump the dependency in the web and vscode packages to ^0.6.0. The new AdmZip(buffer) and extractAllTo(dir, overwrite) APIs are unchanged, so no call-site adaptation is needed. Add a vitest regression test for the ClawdHub install path that builds a real ZIP with adm-zip and asserts extractAllTo restores files (including nested subdirectories) into the target skill dir. --- bun.lock | 6 +- packages/vscode/package.json | 2 +- packages/web/package.json | 2 +- .../skills-catalog/clawdhub/install.test.js | 100 ++++++++++++++++++ 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 packages/web/server/lib/skills-catalog/clawdhub/install.test.js diff --git a/bun.lock b/bun.lock index cd8e95ab..1edf6ebc 100644 --- a/bun.lock +++ b/bun.lock @@ -240,7 +240,7 @@ "dependencies": { "@openchamber/ui": "workspace:*", "@opencode-ai/sdk": "1.18.12", - "adm-zip": "^0.5.16", + "adm-zip": "^0.6.0", "jsonc-parser": "^3.3.1", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -268,7 +268,7 @@ "@octokit/rest": "^22.0.1", "@opencode-ai/sdk": "1.18.12", "@simplewebauthn/server": "13.3.1", - "adm-zip": "^0.5.16", + "adm-zip": "^0.6.0", "bun-pty": "^0.4.5", "compression": "^1.8.1", "cron-parser": "^4.9.0", @@ -1487,7 +1487,7 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + "adm-zip": ["adm-zip@0.6.0", "", {}, "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], diff --git a/packages/vscode/package.json b/packages/vscode/package.json index feea88ca..7959a12d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -245,7 +245,7 @@ "dependencies": { "@openchamber/ui": "workspace:*", "@opencode-ai/sdk": "1.18.12", - "adm-zip": "^0.5.16", + "adm-zip": "^0.6.0", "jsonc-parser": "^3.3.1", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/packages/web/package.json b/packages/web/package.json index 46b04352..51cfdbf5 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -27,7 +27,7 @@ "@octokit/rest": "^22.0.1", "@opencode-ai/sdk": "1.18.12", "@simplewebauthn/server": "13.3.1", - "adm-zip": "^0.5.16", + "adm-zip": "^0.6.0", "bun-pty": "^0.4.5", "compression": "^1.8.1", "cron-parser": "^4.9.0", diff --git a/packages/web/server/lib/skills-catalog/clawdhub/install.test.js b/packages/web/server/lib/skills-catalog/clawdhub/install.test.js new file mode 100644 index 00000000..e57a29ba --- /dev/null +++ b/packages/web/server/lib/skills-catalog/clawdhub/install.test.js @@ -0,0 +1,100 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import AdmZip from 'adm-zip'; + +// Mock the ClawdHub network client so no real HTTP happens. The download +// function is what feeds the ZIP buffer into adm-zip inside install.js. +vi.mock('./api.js', () => ({ + downloadClawdHubSkill: vi.fn(), + fetchClawdHubSkillInfo: vi.fn(), +})); + +const { downloadClawdHubSkill } = await import('./api.js'); +const { installSkillsFromClawdHub } = await import('./install.js'); + +/** + * Build a real ZIP archive with adm-zip (the dependency under test). + * Returns the raw Buffer, mirroring what downloadClawdHubSkill resolves to. + */ +function buildSkillZip(entries) { + const zip = new AdmZip(); + for (const [entryName, content] of Object.entries(entries)) { + zip.addFile(entryName, Buffer.from(content, 'utf8')); + } + return zip.toBuffer(); +} + +describe('installSkillsFromClawdHub (adm-zip extraction path)', () => { + let userSkillDir; + + beforeEach(async () => { + // Keep the target dir under os.tmpdir() so the temp->target rename in + // install.js stays on one filesystem (avoids EXDEV cross-device errors). + userSkillDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'clawdhub-test-skills-')); + vi.clearAllMocks(); + }); + + afterEach(async () => { + await fs.promises.rm(userSkillDir, { recursive: true, force: true }).catch(() => {}); + }); + + it('extracts a real ZIP (incl. nested subdirectories) into the target skill dir', async () => { + const skillMd = 'name: demo-skill\ndescription: adm-zip extraction regression guard\n'; + const nested = 'nested file content for subdirectory extraction check\n'; + downloadClawdHubSkill.mockResolvedValue( + buildSkillZip({ 'SKILL.md': skillMd, 'nested/data.txt': nested }), + ); + + const result = await installSkillsFromClawdHub({ + scope: 'user', + targetSource: 'opencode', + userSkillDir, + // Non-'latest' version avoids the fetchClawdHubSkillInfo resolve branch. + selections: [{ clawdhub: { slug: 'demo-skill', version: '1.0.0' } }], + }); + + expect(result.ok).toBe(true); + expect(result.installed).toEqual([ + { skillName: 'demo-skill', scope: 'user', source: 'opencode' }, + ]); + expect(result.skipped).toEqual([]); + + // downloadClawdHubSkill received the resolved (non-latest) version. + expect(downloadClawdHubSkill).toHaveBeenCalledWith('demo-skill', '1.0.0'); + + // adm-zip actually wrote the files, preserving the nested subdirectory. + const targetDir = path.join(userSkillDir, 'demo-skill'); + const skillMdPath = path.join(targetDir, 'SKILL.md'); + const nestedPath = path.join(targetDir, 'nested', 'data.txt'); + + expect(fs.existsSync(skillMdPath)).toBe(true); + expect(fs.existsSync(nestedPath)).toBe(true); + expect(fs.readFileSync(skillMdPath, 'utf8')).toBe(skillMd); + expect(fs.readFileSync(nestedPath, 'utf8')).toBe(nested); + }); + + it('skips a package whose extracted contents lack SKILL.md', async () => { + // Valid ZIP, but no SKILL.md at the root -> install.js must skip it and + // must NOT create the target dir. This exercises the extractAllTo path + // followed by the post-extraction validation. + downloadClawdHubSkill.mockResolvedValue( + buildSkillZip({ 'README.md': 'no skill manifest here\n' }), + ); + + const result = await installSkillsFromClawdHub({ + scope: 'user', + targetSource: 'opencode', + userSkillDir, + selections: [{ clawdhub: { slug: 'broken-skill', version: '1.0.0' } }], + }); + + expect(result.ok).toBe(true); + expect(result.installed).toEqual([]); + expect(result.skipped).toEqual([ + { skillName: 'broken-skill', reason: 'SKILL.md not found in downloaded package' }, + ]); + expect(fs.existsSync(path.join(userSkillDir, 'broken-skill'))).toBe(false); + }); +});