Skip to content

Commit cb22192

Browse files
author
Max Schaefer
authored
Merge pull request #1196 from asger-semmle/shelljs
JS: Add model for shelljs
2 parents 3d2ae00 + 80f4131 commit cb22192

File tree

6 files changed

+317
-0
lines changed

6 files changed

+317
-0
lines changed

change-notes/1.21/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Node.js](http://nodejs.org)
99
- [Firebase](https://firebase.google.com/)
1010
- [Express](https://expressjs.com/)
11+
- [shelljs](https://www.npmjs.com/package/shelljs)
1112

1213
* The security queries now track data flow through Base64 decoders such as the Node.js `Buffer` class, the DOM function `atob`, and a number of npm packages intcluding [`abab`](https://www.npmjs.com/package/abab), [`atob`](https://www.npmjs.com/package/atob), [`btoa`](https://www.npmjs.com/package/btoa), [`base-64`](https://www.npmjs.com/package/base-64), [`js-base64`](https://www.npmjs.com/package/js-base64), [`Base64.js`](https://www.npmjs.com/package/Base64) and [`base64-js`](https://www.npmjs.com/package/base64-js).
1314

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import semmle.javascript.frameworks.PropertyProjection
8080
import semmle.javascript.frameworks.React
8181
import semmle.javascript.frameworks.ReactNative
8282
import semmle.javascript.frameworks.Request
83+
import semmle.javascript.frameworks.ShellJS
8384
import semmle.javascript.frameworks.SQL
8485
import semmle.javascript.frameworks.SocketIO
8586
import semmle.javascript.frameworks.StringFormatters
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* Models the `shelljs` library in terms of `FileSystemAccess` and `SystemCommandExecution`.
3+
*/
4+
import javascript
5+
6+
module ShellJS {
7+
/**
8+
* Gets an import of the `shelljs` or `async-shelljs` module.
9+
*/
10+
DataFlow::SourceNode shelljs() {
11+
result = DataFlow::moduleImport("shelljs") or
12+
result = DataFlow::moduleImport("async-shelljs")
13+
}
14+
15+
/** A member of the `shelljs` library. */
16+
class Member extends DataFlow::SourceNode {
17+
Member::Range range;
18+
19+
Member() { this = range }
20+
21+
/** Gets the name of `shelljs` member being referenced, such as `cat` in `shelljs.cat`. */
22+
string getName() { result = range.getName() }
23+
}
24+
25+
module Member {
26+
/**
27+
* A member of the `shelljs` library.
28+
*
29+
* Can be subclassed to recognize additional values as `shelljs` member functions.
30+
*/
31+
abstract class Range extends DataFlow::SourceNode {
32+
abstract string getName();
33+
}
34+
35+
private class DefaultRange extends Range {
36+
string name;
37+
38+
DefaultRange() { this = shelljs().getAPropertyRead(name) }
39+
40+
override string getName() { result = name }
41+
}
42+
43+
/** The `shelljs.exec` library modelled as a `shelljs` member. */
44+
private class ShellJsExec extends Range {
45+
ShellJsExec() { this = DataFlow::moduleImport("shelljs.exec") }
46+
47+
override string getName() { result = "exec" }
48+
}
49+
}
50+
51+
/**
52+
* A call to one of the functions exported from the `shelljs` library.
53+
*/
54+
class ShellJSCall extends DataFlow::CallNode {
55+
string name;
56+
57+
ShellJSCall() {
58+
exists(Member member |
59+
this = member.getACall() and
60+
name = member.getName()
61+
)
62+
}
63+
64+
/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
65+
string getName() { result = name }
66+
67+
/** Holds if the first argument starts with a `-`, indicating it is an option. */
68+
predicate hasOptionsArg() {
69+
exists(string val |
70+
getArgument(0).mayHaveStringValue(val) and
71+
val.matches("-%")
72+
)
73+
}
74+
75+
/** Gets the `n`th argument after the initial options argument, if any. */
76+
DataFlow::Node getTranslatedArgument(int n) {
77+
if hasOptionsArg() then result = getArgument(n + 1) else result = getArgument(n)
78+
}
79+
}
80+
81+
/**
82+
* A file system access that can't be modelled as a read or a write.
83+
*/
84+
private class ShellJSGenericFileAccess extends FileSystemAccess, ShellJSCall {
85+
ShellJSGenericFileAccess() {
86+
name = "cd" or
87+
name = "cp" or
88+
name = "chmod" or
89+
name = "pushd" or
90+
name = "find" or
91+
name = "ls" or
92+
name = "ln" or
93+
name = "mkdir" or
94+
name = "mv" or
95+
name = "rm" or
96+
name = "touch"
97+
}
98+
99+
override DataFlow::Node getAPathArgument() { result = getAnArgument() }
100+
}
101+
102+
/**
103+
* A `shelljs` call that returns names of existing files.
104+
*/
105+
private class ShellJSFilenameSource extends FileNameSource, ShellJSCall {
106+
ShellJSFilenameSource() {
107+
name = "find" or
108+
name = "ls"
109+
}
110+
}
111+
112+
/**
113+
* A file system access that returns the contents of a file.
114+
*/
115+
private class ShellJSRead extends FileSystemReadAccess, ShellJSCall {
116+
ShellJSRead() {
117+
name = "cat" or
118+
name = "head" or
119+
name = "sort" or
120+
name = "tail" or
121+
name = "uniq"
122+
}
123+
124+
override DataFlow::Node getAPathArgument() { result = getAnArgument() }
125+
126+
override DataFlow::Node getADataNode() { result = this }
127+
}
128+
129+
/**
130+
* A file system access that returns the contents of a file, but where certain arguemnts
131+
* should be treated as patterns, not filenames.
132+
*/
133+
private class ShellJSPatternRead extends FileSystemReadAccess, ShellJSCall {
134+
int offset;
135+
136+
ShellJSPatternRead() {
137+
name = "grep" and offset = 1
138+
or
139+
name = "sed" and offset = 2
140+
}
141+
142+
override DataFlow::Node getAPathArgument() {
143+
// Do not treat regex patterns as filenames.
144+
exists(int arg |
145+
arg >= offset and
146+
result = getTranslatedArgument(arg)
147+
)
148+
}
149+
150+
override DataFlow::Node getADataNode() { result = this }
151+
}
152+
153+
/**
154+
* A call to `shelljs.exec()` modelled as command execution.
155+
*/
156+
private class ShellJSExec extends SystemCommandExecution, ShellJSCall {
157+
ShellJSExec() { name = "exec" }
158+
159+
override DataFlow::Node getACommandArgument() { result = getArgument(0) }
160+
}
161+
162+
/**
163+
* A call to `to()` or `toEnd()` on the `ShellString` object returned from another `shelljs` call,
164+
* such as `shelljs.cat(file1).to(file2)`.
165+
*/
166+
private class ShellJSPipe extends FileSystemWriteAccess, DataFlow::CallNode {
167+
ShellJSPipe() {
168+
exists(string name | this = any(ShellJSCall inner).getAMethodCall(name) |
169+
name = "to" or
170+
name = "toEnd"
171+
)
172+
}
173+
174+
override DataFlow::Node getAPathArgument() {
175+
result = getArgument(0)
176+
}
177+
178+
override DataFlow::Node getADataNode() {
179+
result = getReceiver()
180+
}
181+
}
182+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
test_FileSystemAccess
2+
| tst.js:3:1:3:17 | shelljs.cat(file) |
3+
| tst.js:4:1:4:25 | shelljs ... file2) |
4+
| tst.js:5:1:5:16 | shelljs.cd(file) |
5+
| tst.js:6:1:6:25 | shelljs ... , file) |
6+
| tst.js:7:1:7:31 | shelljs ... , file) |
7+
| tst.js:8:1:8:24 | shelljs ... file2) |
8+
| tst.js:9:1:9:30 | shelljs ... file2) |
9+
| tst.js:10:1:10:37 | shelljs ... file3) |
10+
| tst.js:11:1:11:19 | shelljs.pushd(file) |
11+
| tst.js:12:1:12:25 | shelljs ... , file) |
12+
| tst.js:15:1:15:26 | shelljs ... file2) |
13+
| tst.js:16:1:16:25 | shelljs ... , file) |
14+
| tst.js:17:1:17:31 | shelljs ... , file) |
15+
| tst.js:18:1:18:39 | shelljs ... file2) |
16+
| tst.js:19:1:19:18 | shelljs.head(file) |
17+
| tst.js:20:1:20:24 | shelljs ... , file) |
18+
| tst.js:21:1:21:32 | shelljs ... file2) |
19+
| tst.js:22:1:22:24 | shelljs ... file2) |
20+
| tst.js:23:1:23:30 | shelljs ... file2) |
21+
| tst.js:24:1:24:16 | shelljs.ls(file) |
22+
| tst.js:25:1:25:22 | shelljs ... , file) |
23+
| tst.js:26:1:26:30 | shelljs ... file2) |
24+
| tst.js:27:1:27:24 | shelljs ... file2) |
25+
| tst.js:28:1:28:19 | shelljs.mkdir(file) |
26+
| tst.js:29:1:29:25 | shelljs ... , file) |
27+
| tst.js:30:1:30:33 | shelljs ... file2) |
28+
| tst.js:31:1:31:24 | shelljs ... file2) |
29+
| tst.js:32:1:32:30 | shelljs ... file2) |
30+
| tst.js:33:1:33:17 | shelljs.rm(file1) |
31+
| tst.js:34:1:34:24 | shelljs ... file2) |
32+
| tst.js:35:1:35:30 | shelljs ... file2) |
33+
| tst.js:36:1:36:37 | shelljs ... , file) |
34+
| tst.js:37:1:37:45 | shelljs ... file2) |
35+
| tst.js:38:1:38:43 | shelljs ... , file) |
36+
| tst.js:39:1:39:51 | shelljs ... file2) |
37+
| tst.js:40:1:40:18 | shelljs.sort(file) |
38+
| tst.js:41:1:41:24 | shelljs ... , file) |
39+
| tst.js:42:1:42:32 | shelljs ... file2) |
40+
| tst.js:43:1:43:18 | shelljs.tail(file) |
41+
| tst.js:44:1:44:24 | shelljs ... , file) |
42+
| tst.js:45:1:45:32 | shelljs ... file2) |
43+
| tst.js:46:1:46:26 | shelljs ... file2) |
44+
| tst.js:48:1:48:18 | shelljs.cat(file1) |
45+
| tst.js:48:1:48:28 | shelljs ... (file2) |
46+
| tst.js:49:1:49:18 | shelljs.cat(file1) |
47+
| tst.js:49:1:49:31 | shelljs ... (file2) |
48+
| tst.js:51:1:51:19 | shelljs.touch(file) |
49+
| tst.js:52:1:52:25 | shelljs ... , file) |
50+
| tst.js:53:1:53:33 | shelljs ... file2) |
51+
| tst.js:54:1:54:27 | shelljs ... file2) |
52+
| tst.js:56:1:56:18 | shelljs.uniq(file) |
53+
| tst.js:57:1:57:26 | shelljs ... file2) |
54+
| tst.js:58:1:58:32 | shelljs ... file2) |
55+
test_MissingFileSystemAccess
56+
test_SystemCommandExecution
57+
| tst.js:14:1:14:27 | shelljs ... ts, cb) |
58+
test_FileNameSource
59+
| tst.js:15:1:15:26 | shelljs ... file2) |
60+
| tst.js:24:1:24:16 | shelljs.ls(file) |
61+
| tst.js:25:1:25:22 | shelljs ... , file) |
62+
| tst.js:26:1:26:30 | shelljs ... file2) |
63+
| tst.js:27:1:27:24 | shelljs ... file2) |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import javascript
2+
3+
query predicate test_FileSystemAccess(FileSystemAccess access) { any() }
4+
5+
query predicate test_MissingFileSystemAccess(VarAccess var) {
6+
var.getName().matches("file%") and
7+
not exists(FileSystemAccess access | access.getAPathArgument().asExpr() = var)
8+
}
9+
10+
query predicate test_SystemCommandExecution(SystemCommandExecution exec) { any() }
11+
12+
query predicate test_FileNameSource(FileNameSource exec) { any() }
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import shelljs from 'shelljs';
2+
3+
shelljs.cat(file);
4+
shelljs.cat(file1, file2);
5+
shelljs.cd(file);
6+
shelljs.chmod(mode, file);
7+
shelljs.chmod(opts, mode, file);
8+
shelljs.cp(file1, file2);
9+
shelljs.cp(opts, file1, file2);
10+
shelljs.cp(opts, file1, file2, file3);
11+
shelljs.pushd(file);
12+
shelljs.pushd(opts, file);
13+
shelljs.popd(opts);
14+
shelljs.exec(cmd, opts, cb);
15+
shelljs.find(file1, file2);
16+
shelljs.grep(regex, file);
17+
shelljs.grep(opts, regex, file);
18+
shelljs.grep(opts, regex, file1, file2);
19+
shelljs.head(file);
20+
shelljs.head(opts, file);
21+
shelljs.head(opts, file1, file2);
22+
shelljs.ln(file1, file2);
23+
shelljs.ln(opts, file1, file2);
24+
shelljs.ls(file);
25+
shelljs.ls(opts, file);
26+
shelljs.ls(opts, file1, file2);
27+
shelljs.ls(file1, file2);
28+
shelljs.mkdir(file);
29+
shelljs.mkdir(opts, file);
30+
shelljs.mkdir(opts, file1, file2);
31+
shelljs.mv(file1, file2);
32+
shelljs.mv(opts, file1, file2);
33+
shelljs.rm(file1);
34+
shelljs.rm(file1, file2);
35+
shelljs.rm(opts, file1, file2);
36+
shelljs.sed(regex, replacement, file);
37+
shelljs.sed(regex, replacement, file1, file2);
38+
shelljs.sed(opts, regex, replacement, file);
39+
shelljs.sed(opts, regex, replacement, file1, file2);
40+
shelljs.sort(file);
41+
shelljs.sort(opts, file);
42+
shelljs.sort(opts, file1, file2);
43+
shelljs.tail(file);
44+
shelljs.tail(opts, file);
45+
shelljs.tail(opts, file1, file2);
46+
shelljs.tail(file1, file2);
47+
48+
shelljs.cat(file1).to(file2);
49+
shelljs.cat(file1).toEnd(file2);
50+
51+
shelljs.touch(file);
52+
shelljs.touch(opts, file);
53+
shelljs.touch(opts, file1, file2);
54+
shelljs.touch(file1, file2);
55+
56+
shelljs.uniq(file);
57+
shelljs.uniq(file1, file2);
58+
shelljs.uniq(opts, file1, file2);

0 commit comments

Comments
 (0)