Skip to content

Commit 1232d2d

Browse files
fix(@angular/build): prevent deleting parent directories of project root
Previously, the output path validation only checked for an exact match against the project root. This allowed ancestor directories (e.g. ../ or ../../) to be used as the output path, which would silently delete all their contents including source files. Use path.relative() to detect when the output path is the project root or any ancestor of it, and throw a descriptive error in both cases. Fixes #6485
1 parent f1ed025 commit 1232d2d

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

packages/angular/build/src/utils/delete-output-dir.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { readdir, rm } from 'node:fs/promises';
10-
import { join, resolve } from 'node:path';
10+
import { join, relative, resolve } from 'node:path';
1111

1212
/**
1313
* Delete an output directory, but error out if it's the root of the project.
@@ -18,8 +18,11 @@ export async function deleteOutputDir(
1818
emptyOnlyDirectories?: string[],
1919
): Promise<void> {
2020
const resolvedOutputPath = resolve(root, outputPath);
21-
if (resolvedOutputPath === root) {
22-
throw new Error('Output path MUST not be project root directory!');
21+
const relativePath = relative(resolvedOutputPath, root);
22+
if (!relativePath || !relativePath.startsWith('..')) {
23+
throw new Error(
24+
`Output path "${resolvedOutputPath}" MUST not be the project root directory or a parent of it!`,
25+
);
2326
}
2427

2528
const directoriesToEmpty = emptyOnlyDirectories
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { mkdir, writeFile } from 'node:fs/promises';
10+
import { join } from 'node:path';
11+
import { deleteOutputDir } from './delete-output-dir';
12+
13+
describe('deleteOutputDir', () => {
14+
let root: string;
15+
16+
beforeEach(async () => {
17+
// Use a unique temp directory for each test
18+
const { mkdtemp } = await import('node:fs/promises');
19+
const { tmpdir } = await import('node:os');
20+
root = await mkdtemp(join(tmpdir(), 'ng-test-'));
21+
});
22+
23+
it('should throw when output path is the project root', async () => {
24+
await expectAsync(deleteOutputDir(root, '.')).toBeRejectedWithError(
25+
/MUST not be the project root directory or a parent of it/,
26+
);
27+
});
28+
29+
it('should throw when output path is a parent of the project root', async () => {
30+
await expectAsync(deleteOutputDir(root, '..')).toBeRejectedWithError(
31+
/MUST not be the project root directory or a parent of it/,
32+
);
33+
});
34+
35+
it('should throw when output path is a grandparent of the project root', async () => {
36+
await expectAsync(deleteOutputDir(root, '../..')).toBeRejectedWithError(
37+
/MUST not be the project root directory or a parent of it/,
38+
);
39+
});
40+
41+
it('should not throw when output path is a child of the project root', async () => {
42+
const outputDir = join(root, 'dist');
43+
await mkdir(outputDir, { recursive: true });
44+
await writeFile(join(outputDir, 'old-file.txt'), 'content');
45+
46+
await expectAsync(deleteOutputDir(root, 'dist')).toBeResolved();
47+
});
48+
49+
it('should delete contents of a valid output directory', async () => {
50+
const outputDir = join(root, 'dist');
51+
await mkdir(outputDir, { recursive: true });
52+
await writeFile(join(outputDir, 'old-file.txt'), 'content');
53+
54+
await deleteOutputDir(root, 'dist');
55+
56+
const { readdir } = await import('node:fs/promises');
57+
const entries = await readdir(outputDir);
58+
expect(entries.length).toBe(0);
59+
});
60+
61+
it('should not throw when output directory does not exist', async () => {
62+
await expectAsync(deleteOutputDir(root, 'nonexistent')).toBeResolved();
63+
});
64+
});

0 commit comments

Comments
 (0)