fix(web): don't allow arbitrary code execution in file-server (#9330)

The Node documentation for `exec` states:

> Never pass unsanitized user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution.

The `outputPath`, `options.buildTarget` and `options.maxParallel` come from `nx.json`. Careful crafting of these fields can result in NX executing arbitrary commands.

This patch fixes this by using `execFile`, which does not spawn a shell.
This commit is contained in:
Sorin Davidoi 2022-03-18 21:16:31 +01:00 committed by GitHub
parent cd8c9b0313
commit f5dfb837a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,4 +1,4 @@
import { exec, execSync } from 'child_process'; import { execFile, execFileSync } from 'child_process';
import { ExecutorContext, joinPathFragments } from '@nrwl/devkit'; import { ExecutorContext, joinPathFragments } from '@nrwl/devkit';
import ignore from 'ignore'; import ignore from 'ignore';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
@ -9,34 +9,34 @@ import { workspaceLayout } from '@nrwl/workspace/src/core/file-utils';
function getHttpServerArgs(options: Schema) { function getHttpServerArgs(options: Schema) {
const args = ['-c-1']; const args = ['-c-1'];
if (options.port) { if (options.port) {
args.push(`-p ${options.port}`); args.push(`-p=${options.port}`);
} }
if (options.host) { if (options.host) {
args.push(`-a ${options.host}`); args.push(`-a=${options.host}`);
} }
if (options.ssl) { if (options.ssl) {
args.push(`-S`); args.push(`-S`);
} }
if (options.sslCert) { if (options.sslCert) {
args.push(`-C ${options.sslCert}`); args.push(`-C=${options.sslCert}`);
} }
if (options.sslKey) { if (options.sslKey) {
args.push(`-K ${options.sslKey}`); args.push(`-K=${options.sslKey}`);
} }
if (options.proxyUrl) { if (options.proxyUrl) {
args.push(`-P ${options.proxyUrl}`); args.push(`-P=${options.proxyUrl}`);
} }
if (options.proxyOptions) { if (options.proxyOptions) {
Object.keys(options.proxyOptions).forEach((key) => { Object.keys(options.proxyOptions).forEach((key) => {
args.push(`--proxy-options.${key}`, options.proxyOptions[key]); args.push(`--proxy-options.${key}=options.proxyOptions[key]`);
}); });
} }
return args; return args;
} }
function getBuildTargetCommand(options: Schema) { function getBuildTargetCommand(options: Schema) {
const cmd = [`npx nx run ${options.buildTarget}`]; const cmd = ['npx', 'nx', 'run', options.buildTarget];
if (options.withDeps) { if (options.withDeps) {
cmd.push(`--with-deps`); cmd.push(`--with-deps`);
} }
@ -46,7 +46,7 @@ function getBuildTargetCommand(options: Schema) {
if (options.maxParallel) { if (options.maxParallel) {
cmd.push(`--maxParallel=${options.maxParallel}`); cmd.push(`--maxParallel=${options.maxParallel}`);
} }
return cmd.join(' '); return cmd;
} }
function getBuildTargetOutputPath(options: Schema, context: ExecutorContext) { function getBuildTargetOutputPath(options: Schema, context: ExecutorContext) {
@ -115,7 +115,8 @@ export default async function* fileServerExecutor(
if (!running) { if (!running) {
running = true; running = true;
try { try {
execSync(getBuildTargetCommand(options), { const [cmd, ...args] = getBuildTargetCommand(options);
execFileSync(cmd, args, {
stdio: [0, 1, 2], stdio: [0, 1, 2],
}); });
} catch {} } catch {}
@ -131,8 +132,12 @@ export default async function* fileServerExecutor(
const outputPath = getBuildTargetOutputPath(options, context); const outputPath = getBuildTargetOutputPath(options, context);
const args = getHttpServerArgs(options); const args = getHttpServerArgs(options);
const serve = exec(`npx http-server ${outputPath} ${args.join(' ')}`, { const serve = execFile('npx', ['http-server', outputPath, ...args], {
cwd: context.root, cwd: context.root,
env: {
FORCE_COLOR: 'true',
...process.env,
},
}); });
const processExitListener = () => { const processExitListener = () => {
serve.kill(); serve.kill();