Skip to content

Commit b45e3e4

Browse files
committed
fix(@angular/build): assert that asset input paths are within workspace root
Ensure that asset patterns defined as objects with an 'input' property are validated to be within the workspace root. - In '@angular/build', introduce a shared 'isSubDirectory' utility and apply it to 'normalizeAssetPatterns'. - In '@angular-devkit/build-angular', apply similar validation during 'normalizeAssetPatterns'. - Add integration tests to prevent regressions from absolute and relative path traversal attempts.
1 parent 0775fe7 commit b45e3e4

5 files changed

Lines changed: 70 additions & 2 deletions

File tree

packages/angular/build/src/builders/application/tests/options/assets_spec.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,29 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
367367

368368
const { error } = await harness.executeOnce({ outputLogsOnException: false });
369369

370-
expect(error?.message).toMatch('asset path must be within the workspace root');
370+
expect(error?.message).toContain('asset path must be within the workspace root');
371+
});
372+
373+
it('fails if asset input option is outside workspace root (relative)', async () => {
374+
harness.useTarget('build', {
375+
...BASE_OPTIONS,
376+
assets: [{ glob: '**/*', input: '../outside', output: '.' }],
377+
});
378+
379+
const { error } = await harness.executeOnce({ outputLogsOnException: false });
380+
381+
expect(error?.message).toContain('asset path must be within the workspace root');
382+
});
383+
384+
it('fails if asset input option is outside workspace root (absolute)', async () => {
385+
harness.useTarget('build', {
386+
...BASE_OPTIONS,
387+
assets: [{ glob: '**/*', input: '/tmp/outside-workspace', output: '.' }],
388+
});
389+
390+
const { error } = await harness.executeOnce({ outputLogsOnException: false });
391+
392+
expect(error?.message).toContain('asset path must be within the workspace root');
371393
});
372394

373395
it('fails if output option is not within project output path', async () => {

packages/angular/build/src/utils/normalize-asset-patterns.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import assert from 'node:assert';
1010
import { statSync } from 'node:fs';
1111
import * as path from 'node:path';
1212
import { AssetPattern, AssetPatternClass } from '../builders/application/schema';
13+
import { isSubDirectory } from './path';
1314

1415
export function normalizeAssetPatterns(
1516
assetPatterns: AssetPattern[],
@@ -70,6 +71,10 @@ export function normalizeAssetPatterns(
7071

7172
assetPattern = { glob, input, output };
7273
} else {
74+
if (!isSubDirectory(workspaceRoot, assetPattern.input)) {
75+
throw new Error(`The ${assetPattern.input} asset path must be within the workspace root.`);
76+
}
77+
7378
assetPattern.output = path.join('.', assetPattern.output ?? '');
7479
}
7580

packages/angular/build/src/utils/path.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { posix } from 'node:path';
9+
import { normalize, posix, resolve } from 'node:path';
1010
import { platform } from 'node:process';
1111

1212
const WINDOWS_PATH_SEPERATOR_REGEXP = /\\/g;
@@ -35,3 +35,17 @@ const WINDOWS_PATH_SEPERATOR_REGEXP = /\\/g;
3535
export function toPosixPath(path: string): string {
3636
return platform === 'win32' ? path.replace(WINDOWS_PATH_SEPERATOR_REGEXP, posix.sep) : path;
3737
}
38+
39+
/**
40+
* Determines if a path is a subdirectory or file within a parent directory.
41+
*
42+
* @param parent - The parent directory path.
43+
* @param child - The child path to check.
44+
* @returns `true` if the child path is within the parent directory, `false` otherwise.
45+
*/
46+
export function isSubDirectory(parent: string, child: string): boolean {
47+
const normalizedParent = normalize(parent);
48+
const resolvedChild = resolve(parent, child);
49+
50+
return resolvedChild.startsWith(normalizedParent);
51+
}

packages/angular_devkit/build_angular/src/builders/browser/tests/options/assets_spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,28 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
359359
harness.expectFile('dist/subdirectory/test.svg').content.toBe('<svg></svg>');
360360
});
361361

362+
it('fails if asset input option is outside workspace root (relative)', async () => {
363+
harness.useTarget('build', {
364+
...BASE_OPTIONS,
365+
assets: [{ glob: '**/*', input: '../outside', output: '.' }],
366+
});
367+
368+
const { result } = await harness.executeOnce();
369+
370+
expect(result?.error).toMatch('asset path must be within the workspace root');
371+
});
372+
373+
it('fails if asset input option is outside workspace root (absolute)', async () => {
374+
harness.useTarget('build', {
375+
...BASE_OPTIONS,
376+
assets: [{ glob: '**/*', input: '/tmp/outside-workspace', output: '.' }],
377+
});
378+
379+
const { result } = await harness.executeOnce();
380+
381+
expect(result?.error).toMatch('asset path must be within the workspace root');
382+
});
383+
362384
it('fails if output option is not within project output path', async () => {
363385
await harness.writeFile('test.svg', '<svg></svg>');
364386

packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export function normalizeAssetPatterns(
6868

6969
assetPattern = { glob, input, output };
7070
} else {
71+
const resolvedInput = path.resolve(workspaceRoot, assetPattern.input);
72+
if (!resolvedInput.startsWith(workspaceRoot)) {
73+
throw new Error(`The ${assetPattern.input} asset path must be within the workspace root.`);
74+
}
75+
7176
assetPattern.output = path.join('.', assetPattern.output ?? '');
7277
}
7378

0 commit comments

Comments
 (0)