This repository has been archived by the owner on Jul 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
6 changed files
with
347 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"detect-child-process": true | ||
} | ||
} |