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.
This commit is contained in:
Mel0ny
2026-08-06 22:59:08 +03:00
committed by GitHub
parent 2c331e2f8b
commit 7e0e22f6e2
4 changed files with 105 additions and 5 deletions
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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);
});
});