Skip to content

Commit 481b6f5

Browse files
committed
fix(create): reject extraction at the cache root
1 parent 7e306cc commit 481b6f5

2 files changed

Lines changed: 35 additions & 7 deletions

File tree

packages/cli/src/create/__tests__/org-tarball.spec.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ import fs from 'node:fs';
22
import os from 'node:os';
33
import path from 'node:path';
44

5-
import { afterEach, describe, expect, it } from 'vitest';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
66

77
import type { OrgManifest } from '../org-manifest.js';
88
import {
99
cleanupStaleStagingDirs,
10+
ensureOrgPackageExtracted,
1011
normalizeEntryName,
1112
parseEntryMode,
1213
resolveBundledPath,
1314
resolveExtractionDir,
1415
sanitizeHostForPath,
1516
} from '../org-tarball.js';
1617

18+
const { mockGetVpDirs } = vi.hoisted(() => ({ mockGetVpDirs: vi.fn() }));
19+
20+
vi.mock('../../../binding/index.js', () => ({ getVpDirs: mockGetVpDirs }));
21+
1722
describe('resolveBundledPath', () => {
1823
const scratchDirs: string[] = [];
1924

@@ -112,9 +117,9 @@ describe('resolveExtractionDir', () => {
112117
},
113118
);
114119

115-
// Escaping `<root>/<host>/<scope>/create` takes at least four `..` segments.
116-
it.each(['../../../../outside', '../../../../../../outside-write', '/absolute'])(
117-
'rejects a version that escapes the cache root: %s',
120+
// Three `..` segments reach the cache root; four escape it.
121+
it.each(['../../..', '../../../../outside', '../../../../../../outside-write', '/absolute'])(
122+
'rejects a version that resolves to or outside the cache root: %s',
118123
(version) => {
119124
expect(() => resolveExtractionDir(cacheRoot, manifestFor(version))).toThrow(
120125
/escapes the cache root/,
@@ -123,6 +128,29 @@ describe('resolveExtractionDir', () => {
123128
);
124129
});
125130

131+
describe('ensureOrgPackageExtracted', () => {
132+
afterEach(() => {
133+
vi.restoreAllMocks();
134+
});
135+
136+
it('rejects the cache root before filesystem or network activity', async () => {
137+
mockGetVpDirs.mockReturnValue({ cache: path.join(os.tmpdir(), 'vp-org-extraction-cache') });
138+
const exists = vi.spyOn(fs, 'existsSync').mockReturnValue(false);
139+
const mkdir = vi.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined);
140+
const readdir = vi.spyOn(fs.promises, 'readdir').mockResolvedValue([]);
141+
const fetch = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('unexpected download'));
142+
143+
await expect(ensureOrgPackageExtracted(manifestFor('../../..'))).rejects.toThrow(
144+
/escapes the cache root/,
145+
);
146+
147+
expect(exists).not.toHaveBeenCalled();
148+
expect(mkdir).not.toHaveBeenCalled();
149+
expect(readdir).not.toHaveBeenCalled();
150+
expect(fetch).not.toHaveBeenCalled();
151+
});
152+
});
153+
126154
describe('sanitizeHostForPath', () => {
127155
it('passes through plain hostnames untouched', () => {
128156
expect(sanitizeHostForPath('registry.npmjs.org')).toBe('registry.npmjs.org');

packages/cli/src/create/org-tarball.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export function sanitizeHostForPath(host: string): string {
2929
* guarantees `manifest.tarballUrl` is a valid URL, so any parse failure
3030
* here is a real bug worth surfacing.
3131
*
32-
* Check containment here as well, in case a caller skips the version
33-
* validation in `readOrgManifest`.
32+
* Require a strict descendant of `cacheRoot` so sibling staging directories
33+
* stay inside the cache, even if a caller skips `readOrgManifest` validation.
3434
*/
3535
export function resolveExtractionDir(cacheRoot: string, manifest: OrgManifest): string {
3636
const { host } = new URL(manifest.tarballUrl);
@@ -42,7 +42,7 @@ export function resolveExtractionDir(cacheRoot: string, manifest: OrgManifest):
4242
'create',
4343
manifest.version,
4444
);
45-
if (resolvedDir !== resolvedRoot && !resolvedDir.startsWith(`${resolvedRoot}${path.sep}`)) {
45+
if (!resolvedDir.startsWith(`${resolvedRoot}${path.sep}`)) {
4646
throw new Error(`org template extraction path escapes the cache root: ${manifest.version}`);
4747
}
4848
return resolvedDir;

0 commit comments

Comments
 (0)