diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index bc95f33ff8e7..aba4eb07f865 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -32,6 +32,18 @@ import { VERSION } from '../../utilities/version'; class CommandError extends Error {} +export const SHELL_METACHARACTERS = /[&|;$`()<>'"\n\r]/; + +export function validateRegistry(registry: string): void { + if (!URL.canParse(registry)) { + throw new CommandModuleError('Option --registry must be a valid URL.'); + } + + if (SHELL_METACHARACTERS.test(registry)) { + throw new CommandModuleError('Option --registry contains invalid characters.'); + } +} + interface AddCommandArgs extends SchematicsCommandArgs { collection: string; verbose?: boolean; @@ -132,7 +144,9 @@ export default class AddCommandModule return true; } - if (typeof registry === 'string' && URL.canParse(registry)) { + if (typeof registry === 'string') { + validateRegistry(registry); + return true; } diff --git a/packages/angular/cli/src/commands/add/registry-validation.spec.ts b/packages/angular/cli/src/commands/add/registry-validation.spec.ts new file mode 100644 index 000000000000..b2fb8315f8f5 --- /dev/null +++ b/packages/angular/cli/src/commands/add/registry-validation.spec.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { SHELL_METACHARACTERS, validateRegistry } from './cli'; + +describe('registry validation', () => { + describe('SHELL_METACHARACTERS', () => { + it('should match shell metacharacters', () => { + expect(SHELL_METACHARACTERS.test('&')).toBe(true); + expect(SHELL_METACHARACTERS.test('|')).toBe(true); + expect(SHELL_METACHARACTERS.test(';')).toBe(true); + expect(SHELL_METACHARACTERS.test('$')).toBe(true); + expect(SHELL_METACHARACTERS.test(String.fromCharCode(96))).toBe(true); + expect(SHELL_METACHARACTERS.test('(')).toBe(true); + expect(SHELL_METACHARACTERS.test(')')).toBe(true); + expect(SHELL_METACHARACTERS.test('<')).toBe(true); + expect(SHELL_METACHARACTERS.test('>')).toBe(true); + expect(SHELL_METACHARACTERS.test('"')).toBe(true); + expect(SHELL_METACHARACTERS.test("'")).toBe(true); + expect(SHELL_METACHARACTERS.test('\n')).toBe(true); + expect(SHELL_METACHARACTERS.test('\r')).toBe(true); + }); + + it('should not match safe URL characters', () => { + expect(SHELL_METACHARACTERS.test('https://registry.example.com')).toBe(false); + expect(SHELL_METACHARACTERS.test('http://registry.example.com:8080')).toBe(false); + expect(SHELL_METACHARACTERS.test('https://registry.example.com/path')).toBe(false); + }); + }); + + describe('validateRegistry', () => { + it('should reject URLs with shell metacharacters', () => { + expect(() => validateRegistry('https://example.com&cmd')).toThrow( + 'Option --registry contains invalid characters.', + ); + expect(() => validateRegistry('https://example.com|cmd')).toThrow( + 'Option --registry contains invalid characters.', + ); + expect(() => validateRegistry('https://example.com;cmd')).toThrow( + 'Option --registry contains invalid characters.', + ); + expect(() => validateRegistry('https://example.com$cmd')).toThrow( + 'Option --registry contains invalid characters.', + ); + expect(() => validateRegistry('https://example.com`cmd`')).toThrow( + 'Option --registry contains invalid characters.', + ); + expect(() => validateRegistry('https://example.com(cmd)')).toThrow( + 'Option --registry contains invalid characters.', + ); + expect(() => validateRegistry('https://example.com?q=">whoami')).toThrow( + 'Option --registry contains invalid characters.', + ); + }); + + it('should accept valid URLs', () => { + expect(() => validateRegistry('https://registry.example.com')).not.toThrow(); + expect(() => validateRegistry('http://registry.example.com:8080')).not.toThrow(); + expect(() => validateRegistry('https://registry.example.com/path')).not.toThrow(); + }); + + it('should reject invalid URLs', () => { + expect(() => validateRegistry('not-a-url')).toThrow( + 'Option --registry must be a valid URL.', + ); + }); + }); +}); diff --git a/packages/angular/cli/src/package-managers/host.ts b/packages/angular/cli/src/package-managers/host.ts index 90f426f9ab71..e382ed1efa46 100644 --- a/packages/angular/cli/src/package-managers/host.ts +++ b/packages/angular/cli/src/package-managers/host.ts @@ -107,7 +107,6 @@ export interface Host { */ export const NodeJS_HOST: Host = { stat, - requiresQuoting: platform() === 'win32', mkdir, readFile: (path: string) => readFile(path, { encoding: 'utf8' }), copyFile: (src, dest) => copyFile(src, dest, constants.COPYFILE_FICLONE), @@ -157,9 +156,7 @@ export const NodeJS_HOST: Host = { cwd: options.cwd, env, } satisfies SpawnOptions; - const childProcess = isWin32 - ? spawn(`${command} ${args.join(' ')}`, spawnOptions) - : spawn(command, args, spawnOptions); + const childProcess = spawn(command, args, spawnOptions); let stdout = ''; childProcess.stdout?.on('data', (data) => (stdout += data.toString()));