Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Commit

Permalink
Added new detect-child-process rule (#252) (#855)
Browse files Browse the repository at this point in the history
* Added new `detect-child-process` rule (#252)

* Minor docs changes (#252)
  • Loading branch information
soon authored and Josh Goldberg committed Apr 24, 2019
1 parent f3229e0 commit b819421
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ We recommend you specify exact versions of lint libraries, including `tslint-mic
</td>
<td>1.0</td>
</tr>
<tr>
<td>
<code>detect-child-process</code>
</td>
<td>
Detects usages of child_process and especially child_process.exec() with a non-literal first argument.
It is dangerous to pass a string constructed at runtime as the first argument to the <code>child_process.exec()</code>.
<code>child_process.exec(cmd)</code> runs <code>cmd</code> as a shell command which could allow an attacker to execute malicious code injected into <code>cmd</code>.
Instead of <code>child_process.exec(cmd)</code> you should use <code>child_process.spawn(cmd)</code> or specify the command as a literal, e.g. <code>child_process.exec('ls')</code>.
</td>
<td>@next</td>
</tr>
<tr>
<td>
<code>export-name</code>
Expand Down
1 change: 1 addition & 0 deletions configs/latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": ["./recommended.json"],
"rulesDirectory": ["../"],
"rules": {
"detect-child-process": true,
"react-a11y-iframes": true,
"react-a11y-mouse-event-has-key-event": true,
"void-zero": true
Expand Down
1 change: 1 addition & 0 deletions cwe_descriptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"75": "Failure to Sanitize Special Elements into a Different Plane (Special Element Injection)",
"79": "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')",
"85": "Doubled Character XSS Manipulations",
"88": "Argument Injection or Modification",
"95": "Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')",
"116": "Improper Encoding or Escaping of Output",
"157": "Failure to Sanitize Paired Delimiters",
Expand Down
206 changes: 206 additions & 0 deletions src/detectChildProcessRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import * as Lint from 'tslint';
import * as tsutils from 'tsutils';
import * as ts from 'typescript';
import { AstUtils } from './utils/AstUtils';

import { ExtendedMetadata } from './utils/ExtendedMetadata';

const FORBIDDEN_IMPORT_FAILURE_STRING: string = 'Found child_process import';
const FOUND_EXEC_FAILURE_STRING: string = 'Found child_process.exec() with non-literal first argument';
const FORBIDDEN_MODULE_NAME = 'child_process';
const FORBIDDEN_FUNCTION_NAME = 'exec';

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: ExtendedMetadata = {
ruleName: 'detect-child-process',
type: 'maintainability',
description: 'Detects instances of child_process and child_process.exec',
rationale: Lint.Utils.dedent`
It is dangerous to pass a string constructed at runtime as the first argument to the child_process.exec().
<code>child_process.exec(cmd)</code> runs <code>cmd</code> as a shell command which allows attacker
to execute malicious code injected into <code>cmd</code> string.
Instead of <code>child_process.exec(cmd)</code> you should use <code>child_process.spawn(cmd)</code>
or specify the command as a literal, e.g. <code>child_process.exec('ls')</code>.
`,
options: null, // tslint:disable-line:no-null-keyword
optionsDescription: '',
typescriptOnly: true,
issueClass: 'SDL',
issueType: 'Error',
severity: 'Important',
level: 'Opportunity for Excellence',
group: 'Security',
commonWeaknessEnumeration: '88'
};

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}

function getProhibitedImportedNames(namedImports: ts.NamedImports) {
return namedImports.elements
.filter(x => {
let originalIdentifier: ts.Identifier;

if (x.propertyName === undefined) {
originalIdentifier = x.name;
} else {
originalIdentifier = x.propertyName;
}
return tsutils.getIdentifierText(originalIdentifier) === FORBIDDEN_FUNCTION_NAME;
})
.map(x => tsutils.getIdentifierText(x.name));
}

function isNotUndefined<TValue>(value: TValue | undefined): value is TValue {
return value !== undefined;
}

function getProhibitedBoundNames(namedBindings: ts.ObjectBindingPattern) {
return namedBindings.elements
.filter(x => {
if (!ts.isIdentifier(x.name)) {
return false;
}
let importedName: string | undefined;

if (x.propertyName === undefined) {
importedName = tsutils.getIdentifierText(x.name);
} else {
if (ts.isIdentifier(x.propertyName)) {
importedName = tsutils.getIdentifierText(x.propertyName);
} else if (ts.isStringLiteral(x.propertyName)) {
importedName = x.propertyName.text;
}
}
return importedName === FORBIDDEN_FUNCTION_NAME;
})
.map(x => {
if (ts.isIdentifier(x.name)) {
return tsutils.getIdentifierText(x.name);
}
return undefined;
})
.filter(isNotUndefined);
}

function walk(ctx: Lint.WalkContext<void>) {
const childProcessModuleAliases = new Set<string>();
const childProcessFunctionAliases = new Set<string>();

function processImport(node: ts.Node, moduleAlias: string | undefined, importedFunctionsAliases: string[], importedModuleName: string) {
if (importedModuleName === FORBIDDEN_MODULE_NAME) {
ctx.addFailureAt(node.getStart(), node.getWidth(), FORBIDDEN_IMPORT_FAILURE_STRING);
if (moduleAlias !== undefined) {
childProcessModuleAliases.add(moduleAlias);
}
importedFunctionsAliases.forEach(x => childProcessFunctionAliases.add(x));
}
}

function processRequire(node: ts.CallExpression) {
const functionTarget = AstUtils.getFunctionTarget(node);

if (functionTarget !== undefined || node.arguments.length === 0) {
return;
}

const firstArg = node.arguments[0];
if (tsutils.isStringLiteral(firstArg) && firstArg.text === FORBIDDEN_MODULE_NAME) {
let alias: string | undefined;
let importedNames: string[] = [];

if (tsutils.isVariableDeclaration(node.parent)) {
if (tsutils.isIdentifier(node.parent.name)) {
alias = tsutils.getIdentifierText(node.parent.name);
} else if (tsutils.isObjectBindingPattern(node.parent.name)) {
importedNames = getProhibitedBoundNames(node.parent.name);
}
}

processImport(node, alias, importedNames, firstArg.text);
}
}

function isProhibitedCall(node: ts.CallExpression): boolean {
const functionName: string = AstUtils.getFunctionName(node);
const functionTarget = AstUtils.getFunctionTarget(node);
const hasNonStringLiteralFirstArgument = node.arguments.length > 0 && !tsutils.isStringLiteral(node.arguments[0]);

if (functionTarget === undefined) {
return childProcessFunctionAliases.has(functionName) && hasNonStringLiteralFirstArgument;
}

return (
childProcessModuleAliases.has(functionTarget) && functionName === FORBIDDEN_FUNCTION_NAME && hasNonStringLiteralFirstArgument
);
}

function processCallExpression(node: ts.CallExpression) {
const functionName: string = AstUtils.getFunctionName(node);

if (functionName === 'require') {
processRequire(node);
}

if (isProhibitedCall(node)) {
ctx.addFailureAt(node.getStart(), node.getWidth(), FOUND_EXEC_FAILURE_STRING);
}
}

function processImportDeclaration(node: ts.ImportDeclaration) {
if (!tsutils.isStringLiteral(node.moduleSpecifier)) {
return;
}

const moduleName: string = node.moduleSpecifier.text;

let alias: string | undefined;
let importedNames: string[] = [];

if (node.importClause !== undefined) {
if (node.importClause.name !== undefined) {
alias = tsutils.getIdentifierText(node.importClause.name);
}
if (node.importClause.namedBindings !== undefined) {
if (tsutils.isNamespaceImport(node.importClause.namedBindings)) {
alias = tsutils.getIdentifierText(node.importClause.namedBindings.name);
} else if (tsutils.isNamedImports(node.importClause.namedBindings)) {
importedNames = getProhibitedImportedNames(node.importClause.namedBindings);
}
}
}

processImport(node, alias, importedNames, moduleName);
}

function processImportEqualsDeclaration(node: ts.ImportEqualsDeclaration) {
if (tsutils.isExternalModuleReference(node.moduleReference)) {
const moduleRef: ts.ExternalModuleReference = node.moduleReference;
if (tsutils.isStringLiteral(moduleRef.expression)) {
const moduleName: string = moduleRef.expression.text;
const alias: string = node.name.text;
processImport(node, alias, [], moduleName);
}
}
}

function cb(node: ts.Node): void {
if (tsutils.isImportEqualsDeclaration(node)) {
processImportEqualsDeclaration(node);
}

if (tsutils.isImportDeclaration(node)) {
processImportDeclaration(node);
}

if (tsutils.isCallExpression(node)) {
processCallExpression(node);
}

return ts.forEachChild(node, cb);
}

return ts.forEachChild(ctx.sourceFile, cb);
}
122 changes: 122 additions & 0 deletions tests/detect-child-process/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as child_process from "child_process";
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
import * as child_process_1 from 'child_process';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
import child_process_2 = require('child_process');
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
const child_process_3 = require("child_process");
~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]


import {exec} from 'child_process';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
import {spawn} from 'child_process';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]

const {exec} = require("child_process");
~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
const {spawn} = require("child_process");
~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]

import {exec as someAnotherExec} from 'child_process';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
import {spawn as someAnotherSpawn} from 'child_process';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]

const {exec: someAnotherExec2} = require("child_process");
~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]
const {spawn: someAnotherSpawn2} = require("child_process");
~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process import]

import * as anotherModule from 'anotherModule';


child_process.exec('ls')
child_process.exec('ls', options)
child_process.exec('ls', options, callback)

child_process.exec(cmd)
~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
child_process.exec(cmd, options)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
child_process.exec(cmd, options, callback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]

child_process.spawn('ls')
child_process.spawn(cmd)

child_process_1.exec('ls')
child_process_1.exec('ls', options)
child_process_1.exec('ls', options, callback)

child_process_1.exec(cmd)
~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
child_process_1.exec(cmd, options)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
child_process_1.exec(cmd, options, callback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]

child_process_1.spawn('ls')
child_process_1.spawn(cmd)

child_process_2.exec('ls')
child_process_2.exec('ls', options)
child_process_2.exec('ls', options, callback)

child_process_2.exec(cmd)
~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
child_process_2.exec(cmd, options)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
child_process_2.exec(cmd, callback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]


child_process_2.spawn('ls')
child_process_2.spawn(cmd)

exec('ls')
exec('ls', options)
exec('ls', options, callback)

exec(cmd)
~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
exec(cmd, options)
~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
exec(cmd, options, callback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]

spawn('ls')
spawn(cmd)

someAnotherExec('ls')
someAnotherExec('ls', options)
someAnotherExec('ls', options, callback)

someAnotherExec(cmd)
~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
someAnotherExec(cmd, options)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
someAnotherExec(cmd, options, callback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]

someAnotherSpawn('ls')
someAnotherSpawn(cmd)

someAnotherExec2('ls')
someAnotherExec2('ls', options)
someAnotherExec2('ls', options, callback)

someAnotherExec2(cmd)
~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
someAnotherExec2(cmd, options)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]
someAnotherExec2(cmd, options, callback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found child_process.exec() with non-literal first argument]

someAnotherSpawn2('ls')
someAnotherSpawn2(cmd)

anotherModule.exec(cmd)
anotherModule.exec(cmd, param2)
anotherModule.exec(cmd, param2, param3)

5 changes: 5 additions & 0 deletions tests/detect-child-process/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"detect-child-process": true
}
}

0 comments on commit b819421

Please sign in to comment.