Skip to content

Commit 66b87bd

Browse files
committed
fix(isolation): reject native traversal forms
1 parent ebfca8e commit 66b87bd

3 files changed

Lines changed: 14 additions & 7 deletions

File tree

src/copy-containment.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,27 @@ function assertPinnedRoot(root, label, relativePath) {
7878
}
7979
}
8080

81-
function validateRelativePath(relativePath) {
81+
function validateRelativePath(relativePath, pathApi = path) {
8282
if (typeof relativePath !== 'string' || relativePath.length === 0) {
8383
throw containmentError(relativePath, 'path must be a non-empty relative string');
8484
}
8585
if (relativePath.includes('\0')) {
8686
throw containmentError(relativePath, 'path contains a null byte');
8787
}
88-
if (path.isAbsolute(relativePath) || path.parse(relativePath).root) {
88+
if (pathApi.isAbsolute(relativePath) || pathApi.parse(relativePath).root) {
8989
throw containmentError(relativePath, 'absolute paths are not allowed');
9090
}
9191

92-
const components = relativePath.split(path.sep);
92+
const components =
93+
pathApi.sep === '\\' ? relativePath.split(/[\\/]/) : relativePath.split(pathApi.sep);
9394
if (components.some((component) => component === '' || component === '.' || component === '..')) {
9495
throw containmentError(
9596
relativePath,
9697
'empty, current-directory, and traversal components are not allowed'
9798
);
9899
}
99100

100-
return path.normalize(relativePath);
101+
return pathApi.normalize(relativePath);
101102
}
102103

103104
function resolveSourcePath(boundary, relativePath) {

src/isolation-manager.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ class IsolationManager {
13231323
}
13241324
};
13251325

1326-
function handleEntry(entry, srcPath, relPath, relativePath, ancestorDirectories) {
1326+
function handleEntry(entry, relPath, relativePath, ancestorDirectories) {
13271327
if (entry.isSymbolicLink()) {
13281328
const resolvedSourcePath = resolveSourcePath(copyBoundary, relPath);
13291329
const targetStats = fs.statSync(resolvedSourcePath);
@@ -1365,11 +1365,10 @@ class IsolationManager {
13651365
}
13661366

13671367
const entryName = validateRelativePath(entry.name);
1368-
const srcPath = path.join(currentSrc, entryName);
13691368
const relPath = relativePath ? path.join(relativePath, entryName) : entryName;
13701369

13711370
try {
1372-
handleEntry(entry, srcPath, relPath, relativePath, childAncestors);
1371+
handleEntry(entry, relPath, relativePath, childAncestors);
13731372
} catch (err) {
13741373
if (shouldIgnoreFsError(err)) {
13751374
continue;

tests/isolation-copy-containment.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
CopyContainmentError,
1010
createCopyBoundary,
1111
resolveCopyPath,
12+
validateRelativePath,
1213
} = require('../src/copy-containment');
1314

1415
function writeFlatFiles(directory, count) {
@@ -50,6 +51,12 @@ describe('isolation copy containment', function () {
5051
manager = new IsolationManager();
5152
});
5253

54+
it('rejects either Windows separator before win32 normalization', function () {
55+
for (const unsafePath of ['nested\\..\\..\\escape', 'nested/../../escape']) {
56+
assert.throws(() => validateRelativePath(unsafePath, path.win32), isContainmentError);
57+
}
58+
});
59+
5360
afterEach(function () {
5461
fs.rmSync(fixtureRoot, { recursive: true, force: true });
5562
});

0 commit comments

Comments
 (0)