Repository navigation
JS: Report command injection through child_process argument arrays when shell is enabled - #22978
vishalshetagar wants to merge 3 commits into
Conversation
asgerf
left a comment
There was a problem hiding this comment.
Looks harmless to merge, but without any tests it's hard to be confident that this PR actually does what is claimed by the change note.
| // the rest (exec/execFile) has the options argument as their second last. | ||
| result = this.getParameter(this.getNumArgument() - 2).asSink() | ||
| // exec/execFile may have a callback after the options argument. | ||
| result = this.getParameter([this.getNumArgument() - 2, this.getNumArgument() - 1]).asSink() |
There was a problem hiding this comment.
There's a line earlier in this predicate (line 807) that reads:
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode and // looks like argumentlistThis seems to be at odds with what this PR is claiming to solve. It seems that this filter should be disabled when shell=true.
It would also be appropriate with unit tests for this.
There was a problem hiding this comment.
🟡 Changes recommended
The changes can produce false positives and suppress existing findings by misclassifying options and callbacks, counting post-execution array mutations, and honoring overwritten shell values, with regression coverage also missing.
5 open issues
Distinguish trailing callbacks from exec options · New Exclude options-only overloads from argument-array sinks · New Report array mutations only when values reach the execution · New Resolve the shell option value at the invocation · New Add regression fixtures for shell options and argument-array sinks · New
Review effort: Max
| // the rest (exec/execFile) has the options argument as their second last. | ||
| result = this.getParameter(this.getNumArgument() - 2).asSink() | ||
| // exec/execFile may have a callback after the options argument. | ||
| result = this.getParameter([this.getNumArgument() - 2, this.getNumArgument() - 1]).asSink() |
| exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink | | ||
| this = sink and |
| exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink | | ||
| this = sink and |
| node.asSink() = sys.getOptionsArg() and | ||
| node.getMember("shell").asSink().mayHaveBooleanValue(true) | ||
| exists(DataFlow::Node shell | | ||
| shell = sys.getOptionsArg().getALocalSource().getAPropertyWrite("shell").getRhs() |
| exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink | | ||
| this = sink and |
|
Thanks @asgerf, I’ve pushed an update with regression tests and expected results. The array filter is in getOptionsArg(), so I’ve kept it to distinguish the options object from the argument array. The new tests verify that getArgumentList() still recognizes arrays when shell is enabled. I also addressed Copilot’s findings around unresolved callbacks, options-only calls, array mutations after execution, and overwritten shell settings. All four affected test suites pass. Would appreciate another look! |


Description of the fix
js/command-line-injectionnow reports remote input that reaches the argument array ofspawn/execFilewhen theshelloption is set totrueor a nonempty string, including their synchronous variants.All three routes are now reported by
js/command-line-injection. Argument arrays in these examples withshell: false,shell: "", or noshelloption remain unreported.For
child_process.spawn,spawnSync,execFile, andexecFileSync, the command-injection sink modeling reusesUnsafeShellCommandConstruction::ShellTrueCommandExecutionSink. The sink is the argument array at the execution call, so array mutations must reach that call to be reported.executesArrayAsShellalso recognizes nonempty string-valued shell paths and checks for local writes that overwrite the shell setting before execution.The
child_processmodel distinguishes argument arrays, options objects, and callbacks. It handlesexecFilecalls with or without a callback, excludes options-only calls from argument-array sinks, and preservesjs/useless-use-of-catfindings when callbacks cannot be resolved locally.Testing
Regression fixtures and expected results cover boolean and string-valued shell options, synchronous and promisified calls, callback-free
execFile, forwarded arrays/options, andpush/unshift. Negative cases cover disabled, empty, omitted, and overwritten shell options, options-only calls, constant arguments, and array mutations after execution.The
CommandInjection,UnsafeShellCommandConstruction,UselessUseOfCat, andNodeJSLibtest suites pass.Fixes #22825.