Skip to content

Commit 276bb94

Browse files
committed
refactor: reduce unsafe command arg usage
1 parent 3a64b67 commit 276bb94

File tree

7 files changed

+50
-45
lines changed

7 files changed

+50
-45
lines changed

packages/core/packages/PackageManager.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ export class PackageManager {
8888
const config = ProjectConfig.localConfig();
8989
if (!config.packagesInstalled) {
9090
let command: string;
91-
let managerCommand: string;
91+
const managerCommand = this.getManager();
9292

93-
managerCommand = this.getManager();
9493
switch (managerCommand) {
9594
case "npm":
9695
/* passes through */
@@ -125,47 +124,50 @@ export class PackageManager {
125124
public static removePackage(packageName: string, verbose: boolean = false): boolean {
126125
let command: string;
127126
const managerCommand = this.getManager();
127+
const sanitizePackage = Util.sanitizeShellArg(packageName);
128128
switch (managerCommand) {
129129
case "npm":
130130
/* passes through */
131131
default:
132-
command = `${managerCommand} uninstall ${packageName} --quiet --save`;
132+
command = `${managerCommand} uninstall ${sanitizePackage} --quiet --save`;
133133
break;
134134
}
135135
try {
136136
// tslint:disable-next-line:object-literal-sort-keys
137137
Util.execSync(command, { stdio: "pipe", encoding: "utf8" });
138138
} catch (error) {
139-
Util.log(`Error uninstalling package ${packageName} with ${managerCommand}`);
139+
Util.log(`Error uninstalling package ${sanitizePackage} with ${managerCommand}`);
140140
if (verbose) {
141141
Util.log(error.message);
142142
}
143143
return false;
144144
}
145145

146-
Util.log(`Package ${packageName} uninstalled successfully`);
146+
Util.log(`Package ${sanitizePackage} uninstalled successfully`);
147147
return true;
148148
}
149149

150150
public static addPackage(packageName: string, verbose: boolean = false): boolean {
151151
const managerCommand = this.getManager();
152-
const command = this.getInstallCommand(managerCommand, packageName);
152+
const sanitizePackage = Util.sanitizeShellArg(packageName);
153+
const command = this.getInstallCommand(managerCommand, sanitizePackage);
153154
try {
154155
// tslint:disable-next-line:object-literal-sort-keys
155156
Util.execSync(command, { stdio: "pipe", encoding: "utf8" });
156157
} catch (error) {
157-
Util.log(`Error installing package ${packageName} with ${managerCommand}`);
158+
Util.log(`Error installing package ${sanitizePackage} with ${managerCommand}`);
158159
if (verbose) {
159160
Util.log(error.message);
160161
}
161162
return false;
162163
}
163-
Util.log(`Package ${packageName} installed successfully`);
164+
Util.log(`Package ${sanitizePackage} installed successfully`);
164165
return true;
165166
}
166167

167168
public static async queuePackage(packageName: string, verbose = false) {
168-
const command = this.getInstallCommand(this.getManager(), packageName).replace("--save", "--no-save");
169+
const command = this.getInstallCommand(this.getManager(), Util.sanitizeShellArg(packageName))
170+
.replace("--save", "--no-save");
169171
const [packName, version] = packageName.split(/@(?=[^\/]+$)/);
170172
const packageJSON = this.getPackageJSON();
171173
if (!packageJSON.dependencies) {
@@ -222,7 +224,7 @@ export class PackageManager {
222224
}
223225

224226
public static ensureRegistryUser(config: Config, message: string): boolean {
225-
const fullPackageRegistry = config.igPackageRegistry;
227+
const fullPackageRegistry = Util.sanitizeShellArg(config.igPackageRegistry);
226228
try {
227229
// tslint:disable-next-line:object-literal-sort-keys
228230
Util.execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" });
@@ -281,7 +283,7 @@ export class PackageManager {
281283
}
282284
}
283285

284-
private static getManager(/*config:Config*/): string {
286+
private static getManager(/*config:Config*/): 'npm' {
285287
//stub to potentially swap out managers
286288
return "npm";
287289
}

packages/core/util/Util.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,21 @@ export class Util {
316316
}
317317
}
318318

319+
/**
320+
* Fairly aggressive sanitize removing anything but ASCII, numbers and a few needed chars that have no action:
321+
* - semicolons (:), dots (.), underscores (_) for paths/URLs
322+
* - dashes (-) & forward slashes (/) for packages and paths/URLs
323+
* - at (@), non-leading tilde (~) for package scope & version
324+
* @remarks
325+
* Most shells should be UTF-enabled, but current usage is very limited thus no need for `\p{L}`
326+
*/
327+
public static sanitizeShellArg(arg: string): string {
328+
return arg
329+
.replace(/[^a-z0-9@:_~\^\/\.\-]/gi, '') // remove unsafe chars
330+
.replace(/\^(?!\d)/g, '') // keep only ^<digit>
331+
.replace(/^~/, ''); // remove leading ~
332+
}
333+
319334
/**
320335
* Execute synchronous command with options
321336
* @param command Command to be executed
@@ -348,29 +363,17 @@ export class Util {
348363
}
349364

350365
/**
351-
* Execute synchronous command with options using spawnSync
352-
* @param command Command to be executed
353-
* @param args Command arguments
354-
* @param options Command options
355-
* @throws {Error} On non-zero exit code. Error has 'status', 'signal', 'output', 'stdout', 'stderr'
356-
*/
357-
public static spawnSync(command: string, args: string[], options?: SpawnSyncOptions) {
358-
try {
359-
return spawnSync(command, args, options);
360-
} catch (error) {
361-
// Handle potential process interruption
362-
// Check if the error output ends with "^C"
363-
if (error.stderr && error.stderr.toString().endsWith() === "^C") {
364-
return process.exit();
365-
}
366-
367-
// Handle specific exit codes for different signals
368-
if (error.status === 3221225786 || error.status > 128) {
369-
return process.exit();
370-
}
371-
372-
throw error;
373-
}
366+
* Execute synchronous command with options using spawnSync
367+
* @param command Command to be executed
368+
* NOTE: `spawn` without `shell` (unsafe) is **not** equivalent to `exec` & requires `npm.cmd` to run the correct process on win
369+
* do not call with/add commands that are not known binaries without validating first
370+
* @param args Command arguments
371+
* @param options Command options
372+
* @returns {SpawnSyncReturns} object with status and stdout
373+
* @remarks Consuming code MUST handle the result and check for failure status!
374+
*/
375+
public static spawnSync(command: string, args: string[], options?: Omit<SpawnSyncOptions, 'shell'>) {
376+
return spawnSync(command, args, options);
374377
}
375378

376379
/**
@@ -383,7 +386,7 @@ export class Util {
383386
const options: any = { cwd: path.join(parentRoot, projectName), stdio: [process.stdin, "ignore", "ignore"] };
384387
Util.execSync("git init", options);
385388
Util.execSync("git add .", options);
386-
Util.execSync("git commit -m " + "\"Initial commit for project: " + projectName + "\"", options);
389+
Util.execSync("git commit -m " + "\"Initial commit for project\"", options);
387390
Util.log(Util.greenCheck() + " Git Initialized and Project '" + projectName + "' Committed");
388391
} catch (error) {
389392
Util.error("Git initialization failed. Install Git in order to automatically commit the project.", "yellow");

packages/ng-schematics/src/ng-new/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export function newProject(options: OptionsSchema): Rule {
174174
}
175175
if (!options.skipGit) {
176176
const gitTask = context.addTask(
177-
new RepositoryInitializerTask(options.name, { message: `Initial commit for project: ${options.name}` }),
177+
new RepositoryInitializerTask(options.name, { message: `Initial commit for project` }),
178178
[...installChain] //copy
179179
);
180180
installChain.push(gitTask);

packages/ng-schematics/src/ng-new/index_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe("Schematics ng-new", () => {
152152
authorEmail: undefined,
153153
authorName: undefined,
154154
commit: true,
155-
message: `Initial commit for project: ${workingDirectory}`
155+
message: `Initial commit for project`
156156
};
157157
const expectedStart: RunSchematicTaskOptions<any> = {
158158
collection: null,
@@ -206,7 +206,7 @@ describe("Schematics ng-new", () => {
206206
authorEmail: undefined,
207207
authorName: undefined,
208208
commit: true,
209-
message: `Initial commit for project: ${workingDirectory}`
209+
message: `Initial commit for project`
210210
};
211211
expect(taskOptions.length).toBe(2);
212212
expect(mockProject.upgradeIgniteUIPackages).toHaveBeenCalled();

spec/acceptance/new-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe("New command", () => {
135135
process.chdir(projectName);
136136
expect(fs.existsSync(".git")).toBeTruthy();
137137
expect(Util.execSync("git log -1 --pretty=format:'%s'").toString())
138-
.toMatch("Initial commit for project: " + projectName);
138+
.toMatch("Initial commit for project");
139139
process.chdir("../");
140140
testFolder = "./angularProj";
141141
});

spec/unit/new-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ describe("Unit - New command", () => {
288288

289289
expect(Util.execSync).toHaveBeenCalledWith("git init", jasmine.any(Object));
290290
expect(Util.execSync).toHaveBeenCalledWith("git add .", jasmine.any(Object));
291-
expect(Util.execSync).toHaveBeenCalledWith("git commit -m " + "\"Initial commit for project: " + projectName + "\"",
291+
expect(Util.execSync).toHaveBeenCalledWith("git commit -m \"Initial commit for project\"",
292292
jasmine.any(Object));
293293
expect(Util.log).toHaveBeenCalledWith(
294294
jasmine.stringMatching("Git Initialized and Project '" + projectName + "' Committed")

spec/unit/packageManager-spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ describe("Unit - Package Manager", () => {
236236
await TestPackageManager.ensureIgniteUISource(true, mockTemplateMgr, true);
237237
expect(TestPackageManager.addPackage).toHaveBeenCalledWith(`@infragistics/ignite-ui-full@"~20.1"`, true);
238238
expect(Util.execSync).toHaveBeenCalledWith(
239-
`npm install @infragistics/ignite-ui-full@"~20.1" --quiet --save`,
239+
`npm install @infragistics/ignite-ui-full@~20.1 --quiet --save`,
240240
jasmine.any(Object)
241241
);
242242
expect(TestPackageManager.removePackage).toHaveBeenCalledWith("ignite-ui", true);
@@ -247,17 +247,17 @@ describe("Unit - Package Manager", () => {
247247
await TestPackageManager.ensureIgniteUISource(true, mockTemplateMgr, true);
248248
expect(TestPackageManager.addPackage).toHaveBeenCalledWith(`@infragistics/ignite-ui-full@"^17.1"`, true);
249249
expect(Util.execSync).toHaveBeenCalledWith(
250-
`npm install @infragistics/ignite-ui-full@"^17.1" --quiet --save`,
250+
`npm install @infragistics/ignite-ui-full@^17.1 --quiet --save`,
251251
jasmine.any(Object)
252252
);
253253

254-
mockDeps.dependencies["ignite-ui"] = ">=0.1.0 <0.2.0";
254+
mockDeps.dependencies["ignite-ui"] = ">=0.1.0"; // ranges no longer supported, but check sanitize
255255
mockProjectConfig.project.igniteuiSource = "./node_modules/ignite-ui";
256256
mockTemplateMgr.generateConfig = mockProjectConfig;
257257
await TestPackageManager.ensureIgniteUISource(true, mockTemplateMgr, true);
258-
expect(TestPackageManager.addPackage).toHaveBeenCalledWith(`@infragistics/ignite-ui-full@">=0.1.0 <0.2.0"`, true);
258+
expect(TestPackageManager.addPackage).toHaveBeenCalledWith(`@infragistics/ignite-ui-full@">=0.1.0"`, true);
259259
expect(Util.execSync).toHaveBeenCalledWith(
260-
`npm install @infragistics/ignite-ui-full@">=0.1.0 <0.2.0" --quiet --save`,
260+
`npm install @infragistics/ignite-ui-full@0.1.0 --quiet --save`,
261261
jasmine.any(Object)
262262
);
263263
});

0 commit comments

Comments
 (0)