JS: add query for useless use of cat - #2867
Conversation
|
I am quite interested in the more restrictive query which only flags the most blatantly useless use of exec(`cat ${file}`, function (err, out) {
... out.trim().toLowerCase()); ...
});As that could trivially be replaced by: readFile({file, function (err, out) {
... out.trim().toLowerCase()); ...
}); |
I've re-written the query to only flag the results that can be trivially replaced with a call to I still include reads from constant strings though (because constant-strings doesn't always stay constant as a program matures). Here are some results: https://lgtm.com/query/344037768842122887/ |
|
Oh wow. That is actually more common than I expected, well done. I think this query is more precise than the two siblings mentioned below. Neither of those queries have the
I think you should have a look at https://github.com/Semmle/ql/blob/de66841263665ad15f64fffa13e9e0432735761a/javascript/ql/src/semmle/javascript/Concepts.qll#L13 to flag even more cases. You probably need to add an abstract Some feedback on the alert messages:
The entire call is selected, which means that many lines are selected in the case of inlined callback functions. Use |
| else extraArg = "" | ||
| ) and | ||
| if exists(cat.getCallback()) | ||
| then callback = ", function(" + getCallbackArgs(cat.getCallback()) + ") {...}" |
There was a problem hiding this comment.
we should support arrow functions here as well.
My first iteration of the query actually used the |
|
Here are the results of the revised query on the same set of benchmarks: https://lgtm.com/query/8990960795230963842/ |
esbena
left a comment
There was a problem hiding this comment.
Hmm. The pretty printing makes this query quite complex.
I would like to see a major refactoring:
Can we separate all of the pretty printing into a separate PrettyPrint module and name of the predicates consistently? At the moment, we use "construct", "representation", "print", "get"(!) suffix and prefixes for the pretty printing predicates, that makes it very hard to separate the alerts from the alert messages.
Ideally, the pretty printing should not prevent any alerts from appearing, but that property is not obvious. It would be nice if the pretty printing module was only referred from the .ql file (or implemented therein), and if the alert message had a fallback string for the case where the pretty printing failed to produce a value.
Semantically, I am a bit concerned about https://lgtm.com/projects/g/nodyn/nodyn/snapshot/eb587c6244757699905b2b24b2e4e26384a3dd08/files/src/test/javascript/childProcessSpec.js?sort=name&dir=ASC&mode=heatmap#L13:
var proc = child_process.spawn('/bin/cat', [ 'pom.xml' ]);
proc.stdout.on('data', function(d) {
content += d.toString();
})
proc.on('close', function() {
expect( content.indexOf( '<project xmlns' ) ).toBeGreaterThan(0);
expect( content.indexOf( '</project>' ) ).toBeGreaterThan(0);
helper.testComplete(true);
})The proc.on usage indicates a non-trivial reading of the cat output, even though it is trivial in this case.
| string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) { | ||
| exists(string sync, string extraArg, string callback | | ||
| (if cat.isSync() then sync = "Sync" else sync = "") and | ||
| ( |
There was a problem hiding this comment.
The existence is implied in the true-branches, so this can be de-iffified to:
(
extraArg = ", " + printOptionsArg(cat.getOptionsArg()) + ")" or
extraArg = "" and not exists(cat.getOptionsArg())
) and
callback = constructCallbackString(cat.getCallback()) or
callback = "" and not exists(cat.getCallback))
| candidate.getFileArgument().length() >= 3 and | ||
| // wildcards, pipes, redirections, and multiple files are OK. | ||
| // (The multiple files detection relies on the fileArgument not containing spaces anywhere) | ||
| not candidate.getFileArgument().regexpMatch(".*(\\*|\\||>|<| ).*") and |
There was a problem hiding this comment.
I would $, &, {, }, and the backtick as well.
| func.getNumParameter() = 2 | ||
| or | ||
| // `exec` can use 3 parameters, `readFile` can only use two, so it is OK to have a third parameter if it is unused, | ||
| func.getNumParameter() = 3 and |
There was a problem hiding this comment.
| @@ -0,0 +1,20 @@ | |||
| /** | |||
| * @name Useless use of cat | |||
| * @description Using cat to simply read a file can lead to unintended bugs, and at worst security issues. | |||
There was a problem hiding this comment.
Can we turn this up a notch? We should also have a sentence for each of these bad properties in the qhelp.
| * @description Using cat to simply read a file can lead to unintended bugs, and at worst security issues. | |
| * @description Using `cat`-process to simply read a file is unnecessarily complex, inefficient, unportable, can lead to subtle bugs, or even security vulnerabilities. |
| * @id js/useless-use-of-cat | ||
| * @tags correctness | ||
| * security | ||
| * external/cwe/cwe-078 |
There was a problem hiding this comment.
I think we should drop the cwes here, and add maintainability instead.
If we insist on cwes, then we should also have the ones for path-injection:
* external/cwe/cwe-022
* external/cwe/cwe-023
* external/cwe/cwe-036
* external/cwe/cwe-073
* external/cwe/cwe-099
I agree, I've found the same thing. I think the query should not flag when the process object is used. |
esbena
left a comment
There was a problem hiding this comment.
Getting closer (sorry for high jacking this Draft PR btw).
I haven't looked at the pretty printing implementation this time, but I note that it accounts for more than half of the query. I think we need to check thoroughly that it scales properly.
| DataFlow::Node getOptionsArg() { | ||
| exists(int n | | ||
| n >= 1 and | ||
| // if there is a command-list, then the options is at least the third argument. |
There was a problem hiding this comment.
I agree that this probably holds in practice, but it seems safer to introduce abstract DataFlow::Node getOptionsArg() into SystemCommandExecution`.
|
|
||
| /** | ||
| * Gets the constant string parts from a data-flow node. | ||
| * Either the string is some constant |
| isACallTo(getACatExecuteable()) and | ||
| // There is a file to read, and not just a pair of quotes. | ||
| ( | ||
| not exists(PrettyPrintCatCall::createFileArgument(this)) |
There was a problem hiding this comment.
This is what I was worried about. Can we avoid letting the query results (excluding the message) depend on the pretty printing implementation?
| ) | ||
| ) and | ||
| // wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK. | ||
| not getNonCommandConstantString().regexpMatch(".*(\\*|\\||>|<| |\\$|&|,|\\`).*") and |
There was a problem hiding this comment.
regexpFind allows us to leave out the .*s...
Co-Authored-By: Esben Sparre Andreasen <esbena@github.com>
|
The current results contain plenty of examples of how not to read a file, but it doesn't seem like there are exploitable vulnerabilities among the results. This one is close, but it is saved by a sanitizer in the |
esbena
left a comment
There was a problem hiding this comment.
Another batch of feedback.
I have created https://github.com/github/codeql-javascript-team/issues/71 for the formidable library.
| override DataFlow::Node getOptionsArg() { | ||
| result = getLastArgument() and | ||
| not result = getArgument(0) and | ||
| not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback |
There was a problem hiding this comment.
I am on the fence in suggestion that we move these two instanceof checks to the abstract class, or remove them completely. I assume you have encountered a problem when we did not have the checks? Or is this just a leftover from the catch-all heuristic we had prior to this commit?
At the very least, it would be nice with an explicitly test that exercises these instanceof cases.
There was a problem hiding this comment.
I assume you have encountered a problem when we did not have the checks?
Yep.
All of these command executions methods have variations of the same API: exec(command[, options][, callback]).
The API is implemented using runtime detection of the types of the arguments, so we have to do something similar.
At the very least, it would be nice with an explicitly test that exercises these instanceof cases.
👍
(I found a bug or two while making those tests)
| * Create a string representing the callback `func`. | ||
| */ | ||
| string createCallbackString(DataFlow::FunctionNode func) { | ||
| exists(string args | args = createCallbackArgs(func) | |
There was a problem hiding this comment.
Nit: args should be params, ditto createCallbackArgs.
| result = arg.asExpr().(VarAccess).getVariable().getName() | ||
| or | ||
| // fall back to toString(), but ensure that we don't have dots in the middle. | ||
| result = arg.(DataFlow::ObjectLiteralNode).toString() and not result.regexpMatch(".*\\.\\..*") |
There was a problem hiding this comment.
So if the options argument is config.options, then the message of the alert will become the fallback message of fs.readFile(...), right?
There was a problem hiding this comment.
Not quite.
Currently we don't flag any exec call where there is an options argument with unknown properties.
An options argument config.options will fail that test, so we will not flag it in the first place.
If config.options didn't fail that test, only the options argument part of the fs.readFile call would be replaced with "...".
Co-Authored-By: Esben Sparre Andreasen <esbena@github.com>
|
A performance evaluation shows that the query adds a little execution time to all projects. |
esbena
left a comment
There was a problem hiding this comment.
Final nits.
Ping @mchammer01 for a doc-review.
|
|
||
| private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode { | ||
| int cmdArg; | ||
| int optionsArg; |
There was a problem hiding this comment.
Please explain the meaning of optionsArg = -1 and optionsArg = -2 in a comment here.
Co-Authored-By: Esben Sparre Andreasen <esbena@github.com>
mchammer01
left a comment
There was a problem hiding this comment.
@erik-krogh - I reviewed this PR from an editorial point of view. It looks good.
I have made a few comments, mainly about improving readability for users (and there was a tiny typo). Let me know what you think.
| @@ -0,0 +1,25 @@ | |||
| /** | |||
| * @name Useless use of cat | |||
There was a problem hiding this comment.
Could we find some synonyms here as there is use and useless in the same sentence.
Also should cat be in single quotes? (cat)?
ps - if Useless use is used in other query names, I am ok for us to leave it.
There was a problem hiding this comment.
Useless is used a fair bit in query names, but Useless use is not used anywhere else.
How about Unnecessary use of cat?
I also think that fits the query better.
There was a problem hiding this comment.
There's The Useless Use of Cat Award which inspired this query, so it would be nice to keep some semblance.
Perhaps we should have a more ordinary name for the query, and then add ("Useless Use of Cat") like we did for js/zip-slip.
* @name Arbitrary file write during zip extraction ("Zip Slip")
There was a problem hiding this comment.
(I wrote that reply before seeing Erik's reply)
There was a problem hiding this comment.
I leave the resolution of this to the two of you, I said what I had to say from an editorial point of view and as a non-developer 😃
There was a problem hiding this comment.
@esbena how do you feel about Unnecessary use of cat?
There was a problem hiding this comment.
Lets qualify it a bit more:
Unnecessary use of `cat` process
Co-Authored-By: mc <42146119+mchammer01@users.noreply.github.com>
| <references> | ||
|
|
||
| <li> | ||
| OWASP: <a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>. |
There was a problem hiding this comment.
Lets add a link to the award page: http://porkmail.org/era/unix/award.html#cat. Or is that too unofficial or not serious enough? @mchammer01, what is your opinion here?
There was a problem hiding this comment.
Thanks for checking with me @esbena, I am fine with this 😉
| @@ -0,0 +1,25 @@ | |||
| /** | |||
| * @name Useless use of cat | |||
There was a problem hiding this comment.
There's The Useless Use of Cat Award which inspired this query, so it would be nice to keep some semblance.
Perhaps we should have a more ordinary name for the query, and then add ("Useless Use of Cat") like we did for js/zip-slip.
* @name Arbitrary file write during zip extraction ("Zip Slip")
|
Thanks for all the doc updates @erik-krogh - it looks great 🥇 |
Adds a query that detects useless use of the unix command
cat. For example:These calls can be replaced with calls to
fs.readFile/fs.readFileSync, which are much safer and less error-prone (if e.g. spaces are used in the input).The pattern is not an error in itself, but it is extremely error-prone, and can easily be replaced with a much safer alternative.
I searched GitHub for uses of
catto help decide what the query should flag.I found some cases where
catis used in combination with wildcards and pipes, which cannot be trivially replaced byfs.readFile, so these uses ofcatare not flagged by the query.It looks like many uses of the pattern originate from people copy-pasting unix bash commands without understanding that there is a better way to do the same thing in node.
Here are some examples of what the query currently flags: https://lgtm.com/query/6505146813103244250/
This also flags CVE-2018-13797.
TODO:
exec/execFilebe flagged? (currently they are)