Skip to content

JS: Enhance command injection detection for CLI argument parsing libraries #20151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved modeling of command-line argument parsing libraries [arg](https://www.npmjs.com/package/arg), [args](https://www.npmjs.com/package/args), [command-line-args](https://www.npmjs.com/package/command-line-args) and [commander](https://www.npmjs.com/package/commander)
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,43 @@ private class ArgsParseStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::CallNode call |
call = DataFlow::moduleMember("args", "parse").getACall() or
call = DataFlow::moduleImport(["yargs-parser", "minimist", "subarg"]).getACall()
call =
DataFlow::moduleImport(["yargs-parser", "minimist", "subarg", "yargs/yargs", "yargs"])
Copy link
Preview

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module import list includes both "yargs/yargs" and "yargs" which could cause duplicate taint tracking for the same library. The "yargs" entry should be removed here since yargs is handled separately in the yargs() function below.

Suggested change
DataFlow::moduleImport(["yargs-parser", "minimist", "subarg", "yargs/yargs", "yargs"])
DataFlow::moduleImport(["yargs-parser", "minimist", "subarg", "yargs/yargs"])

Copilot uses AI. Check for mistakes.

.getACall()
|
succ = call and
pred = call.getArgument(0)
)
or
exists(API::Node commanderNode | commanderNode = commander() |
pred = commanderNode.getMember(["parse", "parseAsync"]).getACall().getAnArgument() and
succ =
[
commanderNode.getMember("opts").getACall(), commanderNode.getAMember().asSource(),
commander()
.getMember("action")
.getACall()
.getArgument(0)
.(DataFlow::FunctionNode)
.getAParameter()
]
Copy link
Preview

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic assumes the action callback always receives parameters, but commander actions can be called without parameters. This could miss taint flow when the action function has no parameters but still processes command arguments through other means.

Suggested change
]
// Propagate to both the function node itself and its parameters
as DataFlow::Node
]

Copilot uses AI. Check for mistakes.

)
or
exists(DataFlow::MethodCallNode methodCall | methodCall = yargs() |
pred = methodCall.getReceiver() and
succ = methodCall
)
or
exists(DataFlow::CallNode call, DataFlow::Node options |
call = DataFlow::moduleImport(["arg", "command-line-args"]).getACall() and
succ = call and
options = call.getArgument(1) and
exists(DataFlow::PropWrite write |
write.getBase() = options and
write.getPropertyName() = "argv" and
pred = write.getRhs()
)
)
}
}

Expand All @@ -115,7 +147,9 @@ private API::Node commander() {
* Either directly imported as a module, or through some chained method call.
*/
private DataFlow::SourceNode yargs() {
result = DataFlow::moduleImport("yargs")
result = DataFlow::moduleImport(["yargs", "yargs/yargs"])
or
result = DataFlow::moduleImport(["yargs", "yargs/yargs"]).getACall()
or
// script used to generate list of chained methods: https://gist.github.com/erik-krogh/f8afe952c0577f4b563a993e613269ba
exists(string method |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
| child_process-test.js:75:29:75:31 | cmd | child_process-test.js:73:25:73:31 | req.url | child_process-test.js:75:29:75:31 | cmd | This command line depends on a $@. | child_process-test.js:73:25:73:31 | req.url | user-provided value |
| child_process-test.js:83:19:83:36 | req.query.fileName | child_process-test.js:83:19:83:36 | req.query.fileName | child_process-test.js:83:19:83:36 | req.query.fileName | This command line depends on a $@. | child_process-test.js:83:19:83:36 | req.query.fileName | user-provided value |
| child_process-test.js:94:11:94:35 | "ping " ... ms.host | child_process-test.js:94:21:94:30 | ctx.params | child_process-test.js:94:11:94:35 | "ping " ... ms.host | This command line depends on a $@. | child_process-test.js:94:21:94:30 | ctx.params | user-provided value |
| command-line-libs.js:14:8:14:18 | options.cmd | command-line-libs.js:9:16:9:23 | req.body | command-line-libs.js:14:8:14:18 | options.cmd | This command line depends on a $@. | command-line-libs.js:9:16:9:23 | req.body | user-provided value |
| command-line-libs.js:15:8:15:18 | program.cmd | command-line-libs.js:9:16:9:23 | req.body | command-line-libs.js:15:8:15:18 | program.cmd | This command line depends on a $@. | command-line-libs.js:9:16:9:23 | req.body | user-provided value |
| command-line-libs.js:21:12:21:17 | script | command-line-libs.js:9:16:9:23 | req.body | command-line-libs.js:21:12:21:17 | script | This command line depends on a $@. | command-line-libs.js:9:16:9:23 | req.body | user-provided value |
| command-line-libs.js:29:10:29:24 | parsed['--cmd'] | command-line-libs.js:27:23:27:30 | req.body | command-line-libs.js:29:10:29:24 | parsed['--cmd'] | This command line depends on a $@. | command-line-libs.js:27:23:27:30 | req.body | user-provided value |
| command-line-libs.js:37:8:37:18 | options.cmd | command-line-libs.js:35:62:35:69 | req.body | command-line-libs.js:37:8:37:18 | options.cmd | This command line depends on a $@. | command-line-libs.js:35:62:35:69 | req.body | user-provided value |
| command-line-libs.js:49:8:49:17 | parsed.cmd | command-line-libs.js:42:16:42:23 | req.body | command-line-libs.js:49:8:49:17 | parsed.cmd | This command line depends on a $@. | command-line-libs.js:42:16:42:23 | req.body | user-provided value |
| exec-sh2.js:10:12:10:57 | cp.spaw ... ptions) | exec-sh2.js:14:25:14:31 | req.url | exec-sh2.js:10:40:10:46 | command | This command line depends on a $@. | exec-sh2.js:14:25:14:31 | req.url | user-provided value |
| exec-sh.js:15:12:15:61 | cp.spaw ... ptions) | exec-sh.js:19:25:19:31 | req.url | exec-sh.js:15:44:15:50 | command | This command line depends on a $@. | exec-sh.js:19:25:19:31 | req.url | user-provided value |
| execSeries.js:14:41:14:47 | command | execSeries.js:18:34:18:40 | req.url | execSeries.js:14:41:14:47 | command | This command line depends on a $@. | execSeries.js:18:34:18:40 | req.url | user-provided value |
Expand Down Expand Up @@ -116,6 +122,35 @@ edges
| child_process-test.js:73:15:73:38 | url.par ... , true) | child_process-test.js:73:9:73:49 | cmd | provenance | |
| child_process-test.js:73:25:73:31 | req.url | child_process-test.js:73:15:73:38 | url.par ... , true) | provenance | |
| child_process-test.js:94:21:94:30 | ctx.params | child_process-test.js:94:11:94:35 | "ping " ... ms.host | provenance | |
| command-line-libs.js:9:9:9:34 | args | command-line-libs.js:12:17:12:20 | args | provenance | |
| command-line-libs.js:9:9:9:34 | args | command-line-libs.js:23:29:23:32 | args | provenance | |
| command-line-libs.js:9:16:9:23 | req.body | command-line-libs.js:9:9:9:34 | args | provenance | |
| command-line-libs.js:12:17:12:20 | args | command-line-libs.js:13:19:13:32 | program.opts() | provenance | |
| command-line-libs.js:12:17:12:20 | args | command-line-libs.js:15:8:15:18 | program.cmd | provenance | |
| command-line-libs.js:12:17:12:20 | args | command-line-libs.js:20:14:20:19 | script | provenance | |
| command-line-libs.js:13:9:13:32 | options | command-line-libs.js:14:8:14:14 | options | provenance | |
| command-line-libs.js:13:19:13:32 | program.opts() | command-line-libs.js:13:9:13:32 | options | provenance | |
| command-line-libs.js:14:8:14:14 | options | command-line-libs.js:14:8:14:18 | options.cmd | provenance | |
| command-line-libs.js:20:14:20:19 | script | command-line-libs.js:21:12:21:17 | script | provenance | |
| command-line-libs.js:23:29:23:32 | args | command-line-libs.js:20:14:20:19 | script | provenance | |
| command-line-libs.js:27:11:27:41 | argsArray | command-line-libs.js:28:53:28:61 | argsArray | provenance | |
| command-line-libs.js:27:23:27:30 | req.body | command-line-libs.js:27:11:27:41 | argsArray | provenance | |
| command-line-libs.js:28:11:28:64 | parsed | command-line-libs.js:29:10:29:15 | parsed | provenance | |
| command-line-libs.js:28:20:28:64 | arg({ ' ... rray }) | command-line-libs.js:28:11:28:64 | parsed | provenance | |
| command-line-libs.js:28:53:28:61 | argsArray | command-line-libs.js:28:20:28:64 | arg({ ' ... rray }) | provenance | |
| command-line-libs.js:29:10:29:15 | parsed | command-line-libs.js:29:10:29:24 | parsed['--cmd'] | provenance | |
| command-line-libs.js:35:9:35:83 | options | command-line-libs.js:37:8:37:14 | options | provenance | |
| command-line-libs.js:35:19:35:83 | command ... \| [] }) | command-line-libs.js:35:9:35:83 | options | provenance | |
| command-line-libs.js:35:62:35:69 | req.body | command-line-libs.js:35:19:35:83 | command ... \| [] }) | provenance | |
| command-line-libs.js:37:8:37:14 | options | command-line-libs.js:37:8:37:18 | options.cmd | provenance | |
| command-line-libs.js:42:9:42:34 | args | command-line-libs.js:43:24:43:27 | args | provenance | |
| command-line-libs.js:42:16:42:23 | req.body | command-line-libs.js:42:9:42:34 | args | provenance | |
| command-line-libs.js:43:9:47:12 | parsed | command-line-libs.js:49:8:49:13 | parsed | provenance | |
| command-line-libs.js:43:18:43:28 | yargs(args) | command-line-libs.js:43:18:47:4 | yargs(a ... ue\\n }) | provenance | |
| command-line-libs.js:43:18:47:4 | yargs(a ... ue\\n }) | command-line-libs.js:43:18:47:12 | yargs(a ... parse() | provenance | |
| command-line-libs.js:43:18:47:12 | yargs(a ... parse() | command-line-libs.js:43:9:47:12 | parsed | provenance | |
| command-line-libs.js:43:24:43:27 | args | command-line-libs.js:43:18:43:28 | yargs(args) | provenance | |
| command-line-libs.js:49:8:49:13 | parsed | command-line-libs.js:49:8:49:17 | parsed.cmd | provenance | |
| exec-sh2.js:9:17:9:23 | command | exec-sh2.js:10:40:10:46 | command | provenance | |
| exec-sh2.js:14:9:14:49 | cmd | exec-sh2.js:15:12:15:14 | cmd | provenance | |
| exec-sh2.js:14:15:14:38 | url.par ... , true) | exec-sh2.js:14:9:14:49 | cmd | provenance | |
Expand Down Expand Up @@ -269,6 +304,38 @@ nodes
| child_process-test.js:83:19:83:36 | req.query.fileName | semmle.label | req.query.fileName |
| child_process-test.js:94:11:94:35 | "ping " ... ms.host | semmle.label | "ping " ... ms.host |
| child_process-test.js:94:21:94:30 | ctx.params | semmle.label | ctx.params |
| command-line-libs.js:9:9:9:34 | args | semmle.label | args |
| command-line-libs.js:9:16:9:23 | req.body | semmle.label | req.body |
| command-line-libs.js:12:17:12:20 | args | semmle.label | args |
| command-line-libs.js:13:9:13:32 | options | semmle.label | options |
| command-line-libs.js:13:19:13:32 | program.opts() | semmle.label | program.opts() |
| command-line-libs.js:14:8:14:14 | options | semmle.label | options |
| command-line-libs.js:14:8:14:18 | options.cmd | semmle.label | options.cmd |
| command-line-libs.js:15:8:15:18 | program.cmd | semmle.label | program.cmd |
| command-line-libs.js:20:14:20:19 | script | semmle.label | script |
| command-line-libs.js:21:12:21:17 | script | semmle.label | script |
| command-line-libs.js:23:29:23:32 | args | semmle.label | args |
| command-line-libs.js:27:11:27:41 | argsArray | semmle.label | argsArray |
| command-line-libs.js:27:23:27:30 | req.body | semmle.label | req.body |
| command-line-libs.js:28:11:28:64 | parsed | semmle.label | parsed |
| command-line-libs.js:28:20:28:64 | arg({ ' ... rray }) | semmle.label | arg({ ' ... rray }) |
| command-line-libs.js:28:53:28:61 | argsArray | semmle.label | argsArray |
| command-line-libs.js:29:10:29:15 | parsed | semmle.label | parsed |
| command-line-libs.js:29:10:29:24 | parsed['--cmd'] | semmle.label | parsed['--cmd'] |
| command-line-libs.js:35:9:35:83 | options | semmle.label | options |
| command-line-libs.js:35:19:35:83 | command ... \| [] }) | semmle.label | command ... \| [] }) |
| command-line-libs.js:35:62:35:69 | req.body | semmle.label | req.body |
| command-line-libs.js:37:8:37:14 | options | semmle.label | options |
| command-line-libs.js:37:8:37:18 | options.cmd | semmle.label | options.cmd |
| command-line-libs.js:42:9:42:34 | args | semmle.label | args |
| command-line-libs.js:42:16:42:23 | req.body | semmle.label | req.body |
| command-line-libs.js:43:9:47:12 | parsed | semmle.label | parsed |
| command-line-libs.js:43:18:43:28 | yargs(args) | semmle.label | yargs(args) |
| command-line-libs.js:43:18:47:4 | yargs(a ... ue\\n }) | semmle.label | yargs(a ... ue\\n }) |
| command-line-libs.js:43:18:47:12 | yargs(a ... parse() | semmle.label | yargs(a ... parse() |
| command-line-libs.js:43:24:43:27 | args | semmle.label | args |
| command-line-libs.js:49:8:49:13 | parsed | semmle.label | parsed |
| command-line-libs.js:49:8:49:17 | parsed.cmd | semmle.label | parsed.cmd |
| exec-sh2.js:9:17:9:23 | command | semmle.label | command |
| exec-sh2.js:10:40:10:46 | command | semmle.label | command |
| exec-sh2.js:14:9:14:49 | cmd | semmle.label | cmd |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import express from 'express';
import { Command } from 'commander';
import { exec } from 'child_process';
import arg from 'arg';
const app = express();
app.use(express.json());

app.post('/Command', async (req, res) => {
const args = req.body.args || []; // $ Source
const program = new Command();
program.option('--cmd <value>', 'Command to execute');
program.parse(args, { from: 'user' });
const options = program.opts();
exec(options.cmd); // $ Alert
exec(program.cmd); // $ Alert

const program1 = new Command();
program1
.command('run <script>')
.action((script) => {
exec(script); // $ Alert
});
await program1.parseAsync(args);
});

app.post('/arg', (req, res) => {
const argsArray = req.body.args || []; // $ Source
const parsed = arg({ '--cmd': String }, { argv: argsArray });
exec(parsed['--cmd']); // $ Alert
});

app.post('/commandLineArgs', (req, res) => {
const commandLineArgs = require('command-line-args');
const optionDefinitions = [{ name: 'cmd', type: String }];
const options = commandLineArgs(optionDefinitions, { argv: req.body.args || [] }); // $ Source
if (!options.cmd) return res.status(400).send({ error: 'Missing --cmd' });
exec(options.cmd); // $ Alert
});

app.post('/yargs', (req, res) => {
const yargs = require('yargs/yargs');
const args = req.body.args || []; // $ Source
const parsed = yargs(args).option('cmd', {
type: 'string',
describe: 'Command to execute',
demandOption: true
}).parse();

exec(parsed.cmd); // $ Alert
});