Skip to content
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

JS: provide command execution sinks for execa package #14294

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

amammad
Copy link
Contributor

@amammad amammad commented Sep 22, 2023

Execa package before version 5 has already been modeled but newer versions up to 8 have many new APIs that I've implemented now.
@erik-krogh the SecondOrderCommandInjectionQuery doesn't work for me here. please look at the test for examples, I already put comments on which lines are vulnerable to second-order command injection.

Copy link
Contributor

@erik-krogh erik-krogh left a comment

Choose a reason for hiding this comment

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

Here is a first round of comments, it will take a while before we can get this ready to merge.
This is not an exhaustive review, but it should give you plenty to work on.
I suggest you only continue with the other library models after we've gotten this PR into a mergable state.


The tests in this PR are in general very confusing to read.
You're mixing file-system access with system-command execution, and nothing in the code/comments in tst.js tells me which is which.

You also have plenty of "GOOD" and "BAD" comments, but I can't figure out what they actually mean.
Just as an example: Line 7 and 9 of tst.js are annotated with "GOOD" and "BAD" respectively.
But they both work only on string constants, and are thus completely harmless, and the only thing you're testing is that they are both recognized as system-command-executions (which they should be).
Basically: there is no connection between the test outcomes (the .expected file) and the comments in the test.

Also, the tests are not testing very much.
E.g. your test_SystemCommandExecution is only testing that a command-execution is recognized, not what the command-argument or argument-list of that command-execution is.
Adding some parameters to the test predicates can help.


Another thing that would be really useful is to add some end-to-end tests.
E.g. adding a new JavaScript file in javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/, where test specifically for command-execution, and annotate each line with // OK / // NOT OK similar to how other files in that directory do it.


A reason for second-order-command-injection not working is that you haven't implemented the optional getArgumentList predicate on the relevant class. When I add an implemtation of that predicate to ExecaExec I can get this example to work just fine: execa('git', ['ls-remote', unsafeValue]); // NOT OK.

// GOOD
await $`echo example1`.pipeStderr(`tmp`);
// BAD argument injection
await $`ssh ${"example2"}`.pipeStderr(`tmp`);
Copy link
Contributor

Choose a reason for hiding this comment

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

There is nothing vulnerable on this line? It's all just constant strings getting concatenated.

* The system command execution nodes for `execa.$` or `execa.$.sync` tag functions
*/
class ExecaScriptEec extends SystemCommandExecution, ExecaScriptExpr {
ExecaScriptEec() { name = ["Sync", "ASync"] }
Copy link
Contributor

Choose a reason for hiding this comment

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

This charpred does nothing. name can only ever be "Sync" or ASync".
Also, a boolean called e.g. isSync is better suited than a string called name, because you're really recording whether the method is sync or not, strings just make it confusing.

isExecaShellEnable(this.getParameter(1))
}

override predicate isSync() { name = "execaSync" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe use a boolean instead of name.
(Same comment throghout).

* The Execa input file option
*/
class ExecaRead extends FileSystemReadAccess, DataFlow::Node {
API::Node execaNode;
Copy link
Contributor

Choose a reason for hiding this comment

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

A better name might be execaArg or something similar.
The Node part of the name doesn't really say anything.

Comment on lines +31 to +32
// data is the output of a command so IDK how it can be implemented
override DataFlow::Node getADataNode() { none() }
Copy link
Contributor

Choose a reason for hiding this comment

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

You can't implement that with other than none() here, so this implementation is fine.

}

// Holds if left parameter is the left child of a template literal and returns the template literal
private TemplateLiteral templateLiteralChildAsSink(Expr left) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The naming here is confusing. The way this predicate is used has something to do with sinks, but the predicate itself is not related to sinks.

Maybe something like isFirstTaggedTemplateParameter? And move the TemplateLiteral to be a parameter of the predicate.


query predicate test_SystemCommandExecution(SystemCommandExecution exec) { any() }

query predicate test_FileNameSource(FileNameSource exec) { any() }
Copy link
Contributor

Choose a reason for hiding this comment

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

This test predicate has no results?
Was this supposed to have results?

override DataFlow::Node getArgumentList() {
// $`${Can Not Be sink} ${sink} .. ${sink}`
exists(TemplateLiteral tmpL | templateLiteralChildAsSink(this.asExpr()) = tmpL |
result.asExpr() = tmpL.getAChildExpr+() and
Copy link
Contributor

Choose a reason for hiding this comment

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

I really don't like this +.

Consider something like $${foo} ${bar} .. ${baz(arg1, submethod(subarg))}``
With .getAChildExpr+() you are getting every sub-expression in the above.
So that includes `baz`, `arg1`, etc. Even those are definitely not relevant.

override predicate isSync() { name = "Sync" }

override DataFlow::Node getOptionsArg() {
result = this.asExpr().getAChildExpr*().flow() and result.asExpr() instanceof ObjectExpr
Copy link
Contributor

Choose a reason for hiding this comment

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

I also really don't like that you have a * here.
Similar argument to above.

/**
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
*/
class ExecaCommandExec2 extends SystemCommandExecution, DataFlow::CallNode {
Copy link
Contributor

Choose a reason for hiding this comment

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

Having a name like ExecaCommandExec2 means that something went wrong when you named your classes, because the difference between ExecaCommandExec2 and ExecaCommandExec is very much not clear from their names.

I think your naming might be easier if you convert some classes to predicate like I've suggested in another review comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants