Skip to content

Commit e1c5449

Browse files
authored
Merge pull request github#2867 from erik-krogh/UselessCat
Approved by esbena
2 parents f3b62e1 + 019266e commit e1c5449

File tree

13 files changed

+791
-7
lines changed

13 files changed

+791
-7
lines changed

change-notes/1.24/analysis-javascript.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
| Polynomial regular expression used on uncontrolled data (`js/polynomial-redos`) | security, external/cwe/cwe-730, external/cwe/cwe-400 | Highlights expensive regular expressions that may be used on malicious input. Results are shown on LGTM by default. |
4747
| Prototype pollution in utility function (`js/prototype-pollution-utility`) | security, external/cwe/cwe-400, external/cwe/cwe-471 | Highlights recursive copying operations that are susceptible to prototype pollution. Results are shown on LGTM by default. |
4848
| Unsafe jQuery plugin (`js/unsafe-jquery-plugin`) | Highlights potential XSS vulnerabilities in unsafely designed jQuery plugins. Results are shown on LGTM by default. |
49+
| Unnecessary use of `cat` process (`js/unnecessary-use-of-cat`) | correctness, security, maintainability | Highlights command executions of `cat` where the fs API should be used instead. Results are shown on LGTM by default. |
50+
4951

5052
## Changes to existing queries
5153

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Using the unix command <code>cat</code> only to read a file is an
7+
unnecessarily complex way to achieve something that can be done in a simpler
8+
and safer manner using the Node.js <code>fs.readFile</code> API.
9+
</p>
10+
<p>
11+
The use of <code>cat</code> for simple file reads leads to code that is
12+
unportable, inefficient, complex, and can lead to subtle bugs or even
13+
security vulnerabilities.
14+
</p>
15+
</overview>
16+
<recommendation>
17+
<p>
18+
Use <code>fs.readFile</code> or <code>fs.readFileSync</code> to read files
19+
from the file system.
20+
</p>
21+
</recommendation>
22+
<example>
23+
24+
<p>The following example shows code that reads a file using <code>cat</code>:</p>
25+
26+
<sample src="examples/useless-cat.js"/>
27+
28+
<p>The code in the example will break if the input <code>name</code> contains
29+
special characters (including space). Additionally, it does not work on Windows
30+
and if the input is user-controlled, a command injection attack can happen.</p>
31+
32+
<p>The <code>fs.readFile</code> API should be used to avoid these potential issues: </p>
33+
34+
<sample src="examples/useless-cat-fixed.js"/>
35+
36+
</example>
37+
<references>
38+
39+
<li>OWASP: <a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.</li>
40+
<li>Node.js: <a href="https://nodejs.org/api/fs.html">File System API</a>.</li>
41+
<li><a href="http://porkmail.org/era/unix/award.html#cat">The Useless Use of Cat Award</a>.</li>
42+
43+
44+
</references>
45+
</qhelp>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @name Unnecessary use of `cat` process
3+
* @description Using the `cat` process to read a file is unnecessarily complex, inefficient, unportable, and can lead to subtle bugs, or even security vulnerabilities.
4+
* @kind problem
5+
* @problem.severity error
6+
* @precision high
7+
* @id js/unnecessary-use-of-cat
8+
* @tags correctness
9+
* security
10+
* maintainability
11+
*/
12+
13+
import javascript
14+
import semmle.javascript.security.UselessUseOfCat
15+
import semmle.javascript.RestrictedLocations
16+
17+
from UselessCat cat, string message
18+
where
19+
message = " Can be replaced with: " + PrettyPrintCatCall::createReadFileCall(cat)
20+
or
21+
not exists(PrettyPrintCatCall::createReadFileCall(cat)) and
22+
if cat.isSync()
23+
then message = " Can be replaced with a call to fs.readFileSync(..)."
24+
else message = " Can be replaced with a call to fs.readFile(..)."
25+
select cat.asExpr().(FirstLineOf), "Unnecessary use of `cat` process." + message
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
var fs = require('fs');
2+
3+
module.exports = function (name) {
4+
return fs.readFileSync(name).toString();
5+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
var child_process = require('child_process');
2+
3+
module.exports = function (name) {
4+
return child_process.execSync("cat " + name).toString();
5+
};

javascript/ql/src/semmle/javascript/Concepts.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ abstract class SystemCommandExecution extends DataFlow::Node {
2222
* to the command.
2323
*/
2424
DataFlow::Node getArgumentList() { none() }
25+
26+
/** Holds if the command execution happens synchronously. */
27+
abstract predicate isSync();
28+
29+
/**
30+
* Gets the data-flow node (if it exists) for an options argument.
31+
*/
32+
abstract DataFlow::Node getOptionsArg();
2533
}
2634

2735
/**

javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,22 @@ module NodeJSLib {
621621
// all of the above methods take the argument list as their second argument
622622
result = getArgument(1)
623623
}
624+
625+
override predicate isSync() {
626+
"Sync" = methodName.suffix(methodName.length() - 4)
627+
}
628+
629+
override DataFlow::Node getOptionsArg() {
630+
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
631+
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode and // looks like argumentlist
632+
not result = getArgument(0) and
633+
// fork/spawn and all sync methos always has options as the last argument
634+
if methodName.regexpMatch("fork.*") or methodName.regexpMatch("spawn.*") or methodName.regexpMatch(".*Sync") then
635+
result = getLastArgument()
636+
else
637+
// the rest (exec/execFile) has the options argument as their second last.
638+
result = getArgument(this.getNumArgument() - 2)
639+
}
624640
}
625641

626642
/**

javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ module ShellJS {
160160
override DataFlow::Node getACommandArgument() { result = getArgument(0) }
161161

162162
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
163+
164+
override predicate isSync() {none ()}
165+
166+
override DataFlow::Node getOptionsArg() {
167+
result = getLastArgument() and
168+
not result = getArgument(0) and
169+
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
170+
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
171+
}
163172
}
164173

165174
/**

javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import javascript
77

88
private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode {
99
int cmdArg;
10+
int optionsArg; // either a positive number representing the n'th argument, or a negative number representing the n'th last argument (e.g. -2 is the second last argument).
1011
boolean shell;
12+
boolean sync;
1113

1214
SystemCommandExecutors() {
1315
exists(string mod, DataFlow::SourceNode callee |
1416
exists(string method |
15-
mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false
17+
mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false and optionsArg = -1
1618
or
1719
mod = "execa" and
20+
optionsArg = -1 and
1821
(
1922
shell = false and
2023
(
@@ -30,27 +33,30 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
3033
) and
3134
cmdArg = 0
3235
|
33-
callee = DataFlow::moduleMember(mod, method)
36+
callee = DataFlow::moduleMember(mod, method) and
37+
sync = getSync(method)
3438
)
3539
or
40+
sync = false and
3641
(
3742
shell = false and
3843
(
39-
mod = "cross-spawn" and cmdArg = 0
44+
mod = "cross-spawn" and cmdArg = 0 and optionsArg = -1
4045
or
41-
mod = "cross-spawn-async" and cmdArg = 0
46+
mod = "cross-spawn-async" and cmdArg = 0 and optionsArg = -1
4247
or
43-
mod = "exec-async" and cmdArg = 0
48+
mod = "exec-async" and cmdArg = 0 and optionsArg = -1
4449
or
45-
mod = "execa" and cmdArg = 0
50+
mod = "execa" and cmdArg = 0 and optionsArg = -1
4651
)
4752
or
4853
shell = true and
4954
(
5055
mod = "exec" and
56+
optionsArg = -2 and
5157
cmdArg = 0
5258
or
53-
mod = "remote-exec" and cmdArg = 1
59+
mod = "remote-exec" and cmdArg = 1 and optionsArg = -1
5460
)
5561
) and
5662
callee = DataFlow::moduleImport(mod)
@@ -64,4 +70,30 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
6470
override predicate isShellInterpreted(DataFlow::Node arg) {
6571
arg = getACommandArgument() and shell = true
6672
}
73+
74+
override DataFlow::Node getArgumentList() { shell = false and result = getArgument(1) }
75+
76+
override predicate isSync() { sync = true }
77+
78+
override DataFlow::Node getOptionsArg() {
79+
(
80+
if optionsArg < 0
81+
then
82+
result = getArgument(getNumArgument() + optionsArg) and
83+
getNumArgument() + optionsArg > cmdArg
84+
else result = getArgument(optionsArg)
85+
) and
86+
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
87+
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
88+
}
89+
}
90+
91+
/**
92+
* Gets a boolean reflecting if the name ends with "sync" or "Sync".
93+
*/
94+
bindingset[name]
95+
private boolean getSync(string name) {
96+
if name.suffix(name.length() - 4) = "Sync" or name.suffix(name.length() - 4) = "sync"
97+
then result = true
98+
else result = false
6799
}

0 commit comments

Comments
 (0)