Repository navigation
JS: Report command injection through child_process argument arrays when shell is enabled - #22978
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! |
| override predicate isSync() { methodName.matches("%Sync") } | ||
|
|
||
| /** Holds if `node` is known to be an options object rather than an argument array or callback. */ | ||
| private predicate isOptionsObject(DataFlow::Node node) { |
There was a problem hiding this comment.
| private predicate isOptionsObject(DataFlow::Node node) { | |
| pragma[inline] | |
| private predicate isOptionsObject(DataFlow::Node node) { |
This predicate does not bind this so it needs to be inlined
| --- | ||
| category: minorAnalysis | ||
| --- | ||
| * Command injection modeling now recognizes argument arrays passed to `spawn` and `execFile` when the `shell` option is enabled, including string-valued shell paths and calls to `execFile` without a callback. The model distinguishes argument arrays, options objects, and callbacks, and accounts for local writes to the `shell` option before execution. |
There was a problem hiding this comment.
| * Command injection modeling now recognizes argument arrays passed to `spawn` and `execFile` when the `shell` option is enabled, including string-valued shell paths and calls to `execFile` without a callback. The model distinguishes argument arrays, options objects, and callbacks, and accounts for local writes to the `shell` option before execution. | |
| * The command injection queries now properly recognize argument arrays passed to `spawn` and `execFile` as sinks when the `shell` option is enabled. |
|
Thanks @asgerf, I’ve applied both suggestions: added pragma[inline] to isOptionsObject and updated the change note to your wording. The NodeJSLib tests and formatting checks pass. |
|
Thanks for the contribution @vishalshetagar, and for addressing all my comments! Let's merge this. |
|
Thanks @asgerf for reviewing and merging this, and for the guidance along the way! Glad I could contribute. |


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.