-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
base: main
Are you sure you want to change the base?
Changes from all commits
e8eb9be
e980798
6b4e34d
39170f3
d6508f3
ae4077d
881ea76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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"]) | ||||||||||
.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() | ||||||||||
] | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
) | ||||||||||
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() | ||||||||||
) | ||||||||||
) | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
|
@@ -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 | | ||||||||||
|
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 | ||
}); |
There was a problem hiding this comment.
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.
Copilot uses AI. Check for mistakes.