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

Added new detect-child-process rule (#252) #855

Merged
merged 2 commits into from Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
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
Expand Up @@ -2,6 +2,7 @@
"extends": ["./recommended.json"],
"rulesDirectory": ["../"],
"rules": {
"detect-child-process": true,
"react-a11y-iframes": true,
"void-zero": true
}
Expand Down
1 change: 1 addition & 0 deletions cwe_descriptions.json
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
@@ -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 = {
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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';
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved


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

child_process.exec(cmd)
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
~~~~~~~~~~~~~~~~~~~~~~~ [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
@@ -0,0 +1,5 @@
{
"rules": {
"detect-child-process": true
}
}