Skip to content

Commit 7397058

Browse files
author
Max Schaefer
committed
JavaScript: Add basic model of socket.io.
1 parent 86e646b commit 7397058

25 files changed

+462
-0
lines changed

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import semmle.javascript.frameworks.React
7979
import semmle.javascript.frameworks.ReactNative
8080
import semmle.javascript.frameworks.Request
8181
import semmle.javascript.frameworks.SQL
82+
import semmle.javascript.frameworks.SocketIO
8283
import semmle.javascript.frameworks.StringFormatters
8384
import semmle.javascript.frameworks.UriLibraries
8485
import semmle.javascript.frameworks.Vue
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* Provides classes for working with [socket.io](https://socket.io).
3+
*/
4+
5+
import javascript
6+
private import semmle.javascript.dataflow.InferredTypes
7+
8+
/**
9+
* Provides classes for working with server-side socket.io code.
10+
*
11+
* We model three concepts: servers, namespaces, and sockets. A server
12+
* has one or more namespaces associated with it, each identified by
13+
* a path name. There is always a default namespace associated with the
14+
* path "/". Data flows between client and server side through sockets,
15+
* with each socket belonging to a namespace on a server.
16+
*/
17+
module SocketIO {
18+
/** Gets the name of a method on `EventEmitter` that returns `this`. */
19+
private string chainableEventEmitterMethod() {
20+
result = "off" or
21+
result = "on" or
22+
result = "once" or
23+
result = "prependListener" or
24+
result = "prependOnceListener" or
25+
result = "removeAllListeners" or
26+
result = "removeListener" or
27+
result = "setMaxListeners"
28+
}
29+
30+
/** A data flow node that may produce (that is, create or return) a socket.io server. */
31+
class ServerNode extends DataFlow::SourceNode {
32+
ServerNode() {
33+
// server creation
34+
this = DataFlow::moduleImport("socket.io").getAnInvocation()
35+
or
36+
// alias for `Server`
37+
this = DataFlow::moduleImport("socket.io").getAMemberCall("listen")
38+
or
39+
// invocation of a chainable method
40+
exists(DataFlow::MethodCallNode mcn, string m |
41+
m = "adapter" or
42+
m = "attach" or
43+
m = "bind" or
44+
m = "listen" or
45+
m = "onconnection" or
46+
m = "origins" or
47+
m = "path" or
48+
m = "serveClient" or
49+
m = "set"
50+
|
51+
mcn = any(ServerNode srv).getAMethodCall(m) and
52+
// exclude getter versions
53+
not mcn.getNumArgument() = 0 and
54+
this = mcn
55+
)
56+
}
57+
}
58+
59+
/** A data flow node that may produce a namespace object. */
60+
class NamespaceNode extends DataFlow::SourceNode {
61+
NamespaceNode() {
62+
// namespace lookup
63+
exists(ServerNode srv |
64+
this = srv.getAPropertyRead("sockets")
65+
or
66+
this = srv.getAMethodCall("of")
67+
)
68+
or
69+
// invocation of a chainable method
70+
exists(string m |
71+
m = "binary" or
72+
m = "clients" or
73+
m = "compress" or
74+
m = "emit" or
75+
m = "in" or
76+
m = "send" or
77+
m = "to" or
78+
m = "use" or
79+
m = "write" or
80+
m = chainableEventEmitterMethod()
81+
|
82+
this = any(NamespaceNode ns).getAMethodCall(m)
83+
or
84+
// server objects forward these methods to their default namespace
85+
this = any(ServerNode srv).getAMethodCall(m)
86+
)
87+
or
88+
// invocation of chainable getter method
89+
exists(string m |
90+
m = "json" or
91+
m = "local" or
92+
m = "volatile"
93+
|
94+
this = any(NamespaceNode base).getAPropertyRead(m)
95+
)
96+
}
97+
}
98+
99+
/** A data flow node that may produce a socket object. */
100+
class SocketNode extends DataFlow::SourceNode {
101+
SocketNode() {
102+
// callback accepting a socket
103+
exists(DataFlow::SourceNode base, string connect, DataFlow::MethodCallNode on |
104+
(base instanceof ServerNode or base instanceof NamespaceNode) and
105+
(connect = "connect" or connect = "connection")
106+
|
107+
on = base.getAMethodCall("on") and
108+
on.getArgument(0).mayHaveStringValue(connect) and
109+
this = on.getCallback(1).getParameter(0)
110+
)
111+
or
112+
// invocation of a chainable method
113+
exists(string m |
114+
m = "binary" or
115+
m = "compress" or
116+
m = "disconnect" or
117+
m = "emit" or
118+
m = "in" or
119+
m = "join" or
120+
m = "leave" or
121+
m = "send" or
122+
m = "to" or
123+
m = "use" or
124+
m = "write" or
125+
m = chainableEventEmitterMethod()
126+
|
127+
this = any(SocketNode base).getAMethodCall(m)
128+
)
129+
or
130+
// invocation of a chainable getter method
131+
exists(string m |
132+
m = "broadcast" or
133+
m = "json" or
134+
m = "local" or
135+
m = "volatile"
136+
|
137+
this = any(SocketNode base).getAPropertyRead(m)
138+
)
139+
}
140+
}
141+
142+
/**
143+
* A data flow node representing an API call that receives data from a client.
144+
*/
145+
class ReceiveNode extends DataFlow::MethodCallNode {
146+
SocketNode socket;
147+
148+
DataFlow::Node eventName;
149+
150+
ReceiveNode() {
151+
exists(string on |
152+
on = "addListener" or
153+
on = "on" or
154+
on = "once" or
155+
on = "prependListener" or
156+
on = "prependOnceListener"
157+
|
158+
this = socket.getAMethodCall(on) and
159+
eventName = getArgument(0)
160+
)
161+
}
162+
163+
/** Gets the socket through which data is received. */
164+
SocketNode getSocket() { result = socket }
165+
166+
/** Gets the event name associated with the data, if it can be determined. */
167+
string getEventName() { eventName.mayHaveStringValue(result) }
168+
169+
/** Gets a data flow node representing data received from a client. */
170+
DataFlow::Node getAReceivedItem() { result = getCallback(1).getAParameter() }
171+
}
172+
173+
/**
174+
* A data flow node representing data received from a client, viewed as remote user input.
175+
*/
176+
private class ReceivedItemAsRemoteFlow extends RemoteFlowSource {
177+
ReceivedItemAsRemoteFlow() { this = any(ReceiveNode rercv).getAReceivedItem() }
178+
179+
override string getSourceType() { result = "socket.io client data" }
180+
181+
override predicate isUserControlledObject() { any() }
182+
}
183+
184+
/**
185+
* A data flow node representing an API call that sends data to a client.
186+
*/
187+
class SendNode extends DataFlow::MethodCallNode {
188+
DataFlow::SourceNode base;
189+
190+
int firstDataIndex;
191+
192+
SendNode() {
193+
exists(string m |
194+
(base instanceof ServerNode or base instanceof NamespaceNode or base instanceof SocketNode) and
195+
this = base.getAMethodCall(m)
196+
|
197+
// a call to `emit`
198+
m = "emit" and
199+
firstDataIndex = 1
200+
or
201+
// a call to `send` or `write`
202+
(m = "send" or m = "write") and
203+
firstDataIndex = 0
204+
)
205+
}
206+
207+
/**
208+
* Gets the socket through which data is sent to the client.
209+
*
210+
* This predicate is not defined for broadcasting sends.
211+
*/
212+
SocketNode getSocket() { result = base }
213+
214+
/** Gets the event name associated with the data, if it can be determined. */
215+
string getEventName() {
216+
if firstDataIndex = 1 then getArgument(0).mayHaveStringValue(result) else result = "message"
217+
}
218+
219+
/** Gets a data flow node representing data sent to the client. */
220+
DataFlow::Node getASentItem() {
221+
exists(int i |
222+
result = getArgument(i) and
223+
i >= firstDataIndex and
224+
// exclude last argument if it is a callback
225+
(
226+
i < getNumArgument() - 1 or
227+
not result.analyze().getTheType() = TTFunction()
228+
)
229+
)
230+
}
231+
232+
/** Gets the acknowledgment callback, if any. */
233+
DataFlow::FunctionNode getAck() {
234+
// acknowledgments are only available when sending through a socket
235+
exists(getSocket()) and
236+
result = getLastArgument().getALocalSource()
237+
}
238+
}
239+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
| tst.js:25:10:25:19 | io.sockets |
2+
| tst.js:26:1:26:10 | io.of("/") |
3+
| tst.js:27:1:27:12 | ns.use(auth) |
4+
| tst.js:28:1:28:11 | ns.to(room) |
5+
| tst.js:29:1:29:11 | ns.in(room) |
6+
| tst.js:30:1:30:28 | ns.emit ... event') |
7+
| tst.js:31:1:31:20 | ns.send('a message') |
8+
| tst.js:32:1:32:21 | ns.writ ... ssage') |
9+
| tst.js:33:1:33:14 | ns.clients(cb) |
10+
| tst.js:34:1:34:17 | ns.compress(true) |
11+
| tst.js:35:1:35:16 | ns.binary(false) |
12+
| tst.js:36:1:36:12 | io.use(auth) |
13+
| tst.js:37:1:37:11 | io.to(room) |
14+
| tst.js:38:1:38:11 | io.in(room) |
15+
| tst.js:39:1:39:31 | io.emit ... ssage') |
16+
| tst.js:40:1:40:20 | io.send('a message') |
17+
| tst.js:41:1:41:21 | io.writ ... ssage') |
18+
| tst.js:42:1:42:14 | io.clients(cb) |
19+
| tst.js:43:1:43:17 | io.compress(true) |
20+
| tst.js:44:1:44:16 | io.binary(false) |
21+
| tst.js:45:1:45:7 | ns.json |
22+
| tst.js:46:1:46:11 | ns.volatile |
23+
| tst.js:47:1:47:8 | ns.local |
24+
| tst.js:50:1:66:2 | io.on(' ... cal;\\n}) |
25+
| tst.js:67:1:67:35 | io.on(' ... => {}) |
26+
| tst.js:68:1:68:32 | ns.on(' ... => {}) |
27+
| tst.js:69:1:73:2 | ns.on(' ... {});\\n}) |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
3+
from SocketIO::NamespaceNode ns
4+
select ns
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
| tst.js:70:3:70:35 | socket. ... => {}) | tst.js:69:22:69:27 | socket |
2+
| tst.js:71:3:71:46 | socket. ... => {}) | tst.js:69:22:69:27 | socket |
3+
| tst.js:72:3:72:43 | socket. ... => {}) | tst.js:69:22:69:27 | socket |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
3+
from SocketIO::ReceiveNode rn
4+
select rn, rn.getSocket()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
| tst.js:70:3:70:35 | socket. ... => {}) | tst.js:70:25:70:27 | msg |
2+
| tst.js:71:3:71:46 | socket. ... => {}) | tst.js:71:27:71:31 | data1 |
3+
| tst.js:71:3:71:46 | socket. ... => {}) | tst.js:71:34:71:38 | data2 |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
3+
from SocketIO::ReceiveNode rn
4+
select rn, rn.getAReceivedItem()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| tst.js:70:3:70:35 | socket. ... => {}) | message |
2+
| tst.js:71:3:71:46 | socket. ... => {}) | message |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
3+
from SocketIO::ReceiveNode rn
4+
select rn, rn.getEventName()

0 commit comments

Comments
 (0)