9cd6b08441
node-stream-zip's extract(null, root) writes each entry to path.join(root, entry.name) without neutralising '../', so a crafted archive entry named '../../uploads/logos/evil.svg' escaped the target dir and overwrote arbitrary files (logos, .env, route files → RCE on source deploys). Requires admin with archives.restore. Adds assertZipEntriesWithin() to utils/safePath.js — a lexical containment check run on the entry list BEFORE extract() — and guards both extract sinks: adminArchives.js (the reported route) and picpeakImportService.js (the sibling .picpeak import, same sink). Adds unit tests for traversal, absolute-path, and sibling-prefix entries.
42 lines
1.7 KiB
JavaScript
42 lines
1.7 KiB
JavaScript
const path = require('path');
|
|
const { assertZipEntriesWithin } = require('../../src/utils/safePath');
|
|
|
|
describe('assertZipEntriesWithin (ZIP-slip guard, GHSA-jfhw-fj23-fx6x)', () => {
|
|
const root = path.join('/tmp', 'picpeak-extract-root');
|
|
|
|
it('accepts entries that stay within the extraction root', () => {
|
|
const entries = [
|
|
{ name: 'photo.jpg' },
|
|
{ name: 'category/nested/photo.png' },
|
|
{ name: 'photos_manifest.json' },
|
|
{ name: 'subdir/' },
|
|
];
|
|
expect(() => assertZipEntriesWithin(entries, root)).not.toThrow();
|
|
});
|
|
|
|
it('rejects a parent-traversal entry', () => {
|
|
const entries = [{ name: '../../uploads/logos/evil.svg' }];
|
|
expect(() => assertZipEntriesWithin(entries, root)).toThrow(/escapes the extraction directory/);
|
|
});
|
|
|
|
it('rejects an absolute-path entry', () => {
|
|
const entries = [{ name: '/etc/cron.d/evil' }];
|
|
expect(() => assertZipEntriesWithin(entries, root)).toThrow(/escapes the extraction directory/);
|
|
});
|
|
|
|
it('rejects when a safe entry is mixed with a traversal entry', () => {
|
|
const entries = [{ name: 'ok.jpg' }, { name: '../escape.txt' }];
|
|
expect(() => assertZipEntriesWithin(entries, root)).toThrow(/escapes the extraction directory/);
|
|
});
|
|
|
|
it('tolerates empty / nameless entries', () => {
|
|
expect(() => assertZipEntriesWithin([{}, { name: '' }, null], root)).not.toThrow();
|
|
});
|
|
|
|
it('does not treat a sibling prefix directory as inside the root', () => {
|
|
// root is .../picpeak-extract-root; ../picpeak-extract-root-evil must not pass
|
|
const entries = [{ name: '../picpeak-extract-root-evil/x' }];
|
|
expect(() => assertZipEntriesWithin(entries, root)).toThrow(/escapes the extraction directory/);
|
|
});
|
|
});
|