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:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user