Skip to content

Commit d17d29a

Browse files
authored
Merge pull request #19218 from Napalys/js/upgrade_websocket
JS: Refactor `WebSocket` to use `API` graphs
2 parents 7ed8a85 + 5243f90 commit d17d29a

File tree

12 files changed

+380
-69
lines changed

12 files changed

+380
-69
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Improved detection of `WebSocket` and `SockJS` usage.
5+
* Added data received from `WebSocket` clients as a remote flow source.

javascript/ql/lib/semmle/javascript/frameworks/WebSocket.qll

+78-19
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ private predicate areLibrariesCompatible(
4747
(client = LibraryNames::ws() or client = LibraryNames::websocket())
4848
}
4949

50+
/** Treats `WebSocket` as an entry point for API graphs. */
51+
private class WebSocketEntryPoint extends API::EntryPoint {
52+
WebSocketEntryPoint() { this = "global.WebSocket" }
53+
54+
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("WebSocket") }
55+
}
56+
57+
/** Treats `SockJS` as an entry point for API graphs. */
58+
private class SockJSEntryPoint extends API::EntryPoint {
59+
SockJSEntryPoint() { this = "global.SockJS" }
60+
61+
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("SockJS") }
62+
}
63+
5064
/**
5165
* Provides classes that model WebSockets clients.
5266
*/
@@ -56,7 +70,7 @@ module ClientWebSocket {
5670
/**
5771
* A class that can be used to instantiate a WebSocket instance.
5872
*/
59-
class SocketClass extends DataFlow::SourceNode {
73+
deprecated class SocketClass extends DataFlow::SourceNode {
6074
LibraryName library; // the name of the WebSocket library. Can be one of the libraries defined in `LibraryNames`.
6175

6276
SocketClass() {
@@ -78,13 +92,38 @@ module ClientWebSocket {
7892
LibraryName getLibrary() { result = library }
7993
}
8094

95+
/**
96+
* A class that can be used to instantiate a WebSocket instance.
97+
*/
98+
class WebSocketClass extends API::Node {
99+
LibraryName library; // the name of the WebSocket library. Can be one of the libraries defined in `LibraryNames`.
100+
101+
WebSocketClass() {
102+
this = any(WebSocketEntryPoint e).getANode() and library = websocket()
103+
or
104+
this = API::moduleImport("ws") and library = ws()
105+
or
106+
// the sockjs-client library:https://www.npmjs.com/package/sockjs-client
107+
library = sockjs() and
108+
(
109+
this = API::moduleImport("sockjs-client") or
110+
this = any(SockJSEntryPoint e).getANode()
111+
)
112+
}
113+
114+
/**
115+
* Gets the WebSocket library name.
116+
*/
117+
LibraryName getLibrary() { result = library }
118+
}
119+
81120
/**
82121
* A client WebSocket instance.
83122
*/
84-
class ClientSocket extends EventEmitter::Range, DataFlow::NewNode, ClientRequest::Range {
85-
SocketClass socketClass;
123+
class ClientSocket extends EventEmitter::Range, API::NewNode, ClientRequest::Range {
124+
WebSocketClass socketClass;
86125

87-
ClientSocket() { this = socketClass.getAnInstantiation() }
126+
ClientSocket() { this = socketClass.getAnInvocation() }
88127

89128
/**
90129
* Gets the WebSocket library name.
@@ -115,10 +154,10 @@ module ClientWebSocket {
115154
/**
116155
* A message sent from a WebSocket client.
117156
*/
118-
class SendNode extends EventDispatch::Range, DataFlow::CallNode {
157+
class SendNode extends EventDispatch::Range, API::CallNode {
119158
override ClientSocket emitter;
120159

121-
SendNode() { this = emitter.getAMemberCall("send") }
160+
SendNode() { this = emitter.getReturn().getMember("send").getACall() }
122161

123162
override string getChannel() { result = channelName() }
124163

@@ -145,8 +184,8 @@ module ClientWebSocket {
145184
private DataFlow::FunctionNode getAMessageHandler(
146185
ClientWebSocket::ClientSocket emitter, string methodName
147186
) {
148-
exists(DataFlow::CallNode call |
149-
call = emitter.getAMemberCall(methodName) and
187+
exists(API::CallNode call |
188+
call = emitter.getReturn().getMember(methodName).getACall() and
150189
call.getArgument(0).mayHaveStringValue("message") and
151190
result = call.getCallback(1)
152191
)
@@ -161,7 +200,13 @@ module ClientWebSocket {
161200
WebSocketReceiveNode() {
162201
this = getAMessageHandler(emitter, "addEventListener")
163202
or
164-
this = emitter.getAPropertyWrite("onmessage").getRhs()
203+
this = emitter.getReturn().getMember("onmessage").getAValueReachingSink()
204+
or
205+
exists(DataFlow::MethodCallNode bindCall |
206+
bindCall = emitter.getReturn().getMember("onmessage").getAValueReachingSink() and
207+
bindCall.getMethodName() = "bind" and
208+
this = bindCall.getReceiver().getAFunctionValue()
209+
)
165210
}
166211

167212
override DataFlow::Node getReceivedItem(int i) {
@@ -192,19 +237,30 @@ module ServerWebSocket {
192237
/**
193238
* Gets a server created by a library named `library`.
194239
*/
195-
DataFlow::SourceNode getAServer(LibraryName library) {
240+
deprecated DataFlow::SourceNode getAServer(LibraryName library) {
196241
library = ws() and
197242
result = DataFlow::moduleImport("ws").getAConstructorInvocation("Server")
198243
or
199244
library = sockjs() and
200245
result = DataFlow::moduleImport("sockjs").getAMemberCall("createServer")
201246
}
202247

248+
/**
249+
* Gets a server created by a library named `library`.
250+
*/
251+
API::InvokeNode getAServerInvocation(LibraryName library) {
252+
library = ws() and
253+
result = API::moduleImport("ws").getMember("Server").getAnInvocation()
254+
or
255+
library = sockjs() and
256+
result = API::moduleImport("sockjs").getMember("createServer").getAnInvocation()
257+
}
258+
203259
/**
204260
* Gets a `socket.on("connection", (msg, req) => {})` call.
205261
*/
206262
private DataFlow::CallNode getAConnectionCall(LibraryName library) {
207-
result = getAServer(library).getAMemberCall(EventEmitter::on()) and
263+
result = getAServerInvocation(library).getReturn().getMember(EventEmitter::on()).getACall() and
208264
result.getArgument(0).mayHaveStringValue("connection")
209265
}
210266

@@ -324,15 +380,18 @@ module ServerWebSocket {
324380
result = this.getCallback(1).getParameter(0)
325381
}
326382
}
383+
}
327384

328-
/**
329-
* A data flow node representing data received from a client, viewed as remote user input.
330-
*/
331-
private class ReceivedItemAsRemoteFlow extends RemoteFlowSource {
332-
ReceivedItemAsRemoteFlow() { this = any(ReceiveNode rercv).getReceivedItem(_) }
385+
/**
386+
* A data flow node representing data received from a client or server, viewed as remote user input.
387+
*/
388+
private class ReceivedItemAsRemoteFlow extends RemoteFlowSource {
389+
ReceivedItemAsRemoteFlow() {
390+
this = any(ClientWebSocket::ReceiveNode rercv).getReceivedItem(_) or
391+
this = any(ServerWebSocket::ReceiveNode rercv).getReceivedItem(_)
392+
}
333393

334-
override string getSourceType() { result = "WebSocket client data" }
394+
override string getSourceType() { result = "WebSocket transmitted data" }
335395

336-
override predicate isUserControlledObject() { any() }
337-
}
396+
override predicate isUserControlledObject() { any() }
338397
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import javascript
2+
3+
API::NewNode getAWebSocketInstance() { result instanceof ClientWebSocket::ClientSocket }
4+
5+
from DataFlow::Node handler
6+
where
7+
handler = getAWebSocketInstance().getReturn().getMember("onmessage").asSource()
8+
or
9+
handler = getAWebSocketInstance().getAPropertyWrite("onmessage").getRhs()
10+
select handler, "This is a WebSocket onmessage handler."
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { MyWebSocket, MySockJS, myWebSocketInstance, mySockJSInstance } from './browser.js';
2+
3+
(function () {
4+
const socket = new MyWebSocket('ws://localhost:9080'); // $ clientSocket
5+
6+
socket.addEventListener('open', function (event) {
7+
socket.send('Hi from browser!'); // $ clientSend
8+
});
9+
10+
socket.addEventListener('message', function (event) {
11+
console.log('Message from server ', event.data); // $ remoteFlow
12+
}); // $ clientReceive
13+
14+
socket.onmessage = function (event) {
15+
console.log("Message from server 2", event.data); // $ remoteFlow
16+
}; // $ clientReceive
17+
})();
18+
19+
20+
(function () {
21+
var sock = new MySockJS('http://0.0.0.0:9999/echo'); // $ clientSocket
22+
sock.onopen = function () {
23+
sock.send('test'); // $ clientSend
24+
};
25+
26+
sock.onmessage = function (e) {
27+
console.log('message', e.data); // $ remoteFlow
28+
sock.close();
29+
}; // $ clientReceive
30+
31+
sock.addEventListener('message', function (event) {
32+
console.log('Using addEventListener ', event.data); // $ remoteFlow
33+
}); // $ clientReceive
34+
})();
35+
36+
37+
(function () {
38+
myWebSocketInstance.addEventListener('open', function (event) {
39+
myWebSocketInstance.send('Hi from browser!'); // $ clientSend
40+
});
41+
42+
myWebSocketInstance.addEventListener('message', function (event) {
43+
console.log('Message from server ', event.data); // $ remoteFlow
44+
}); // $ clientReceive
45+
46+
myWebSocketInstance.onmessage = function (event) {
47+
console.log("Message from server 2", event.data); // $ remoteFlow
48+
}; // $ clientReceive
49+
})();
50+
51+
52+
(function () {
53+
mySockJSInstance.onopen = function () {
54+
mySockJSInstance.send('test'); // $ clientSend
55+
};
56+
57+
mySockJSInstance.onmessage = function (e) {
58+
console.log('message', e.data); // $ remoteFlow
59+
mySockJSInstance.close();
60+
}; // $ clientReceive
61+
62+
mySockJSInstance.addEventListener('message', function (event) {
63+
console.log('Using addEventListener ', event.data); // $ remoteFlow
64+
}); // $ clientReceive
65+
})();
66+
67+
68+
const recv_message = function (e) {
69+
console.log('Received message:', e.data); // $ remoteFlow
70+
}; // $ clientReceive
71+
72+
(function () {
73+
myWebSocketInstance.onmessage = recv_message.bind(this);
74+
})();
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
(function () {
2-
const socket = new WebSocket('ws://localhost:8080');
2+
const socket = new WebSocket('ws://localhost:8080'); // $clientSocket
33

44
socket.addEventListener('open', function (event) {
5-
socket.send('Hi from browser!');
5+
socket.send('Hi from browser!'); // $clientSend
66
});
77

88
socket.addEventListener('message', function (event) {
9-
console.log('Message from server ', event.data);
10-
});
9+
console.log('Message from server ', event.data); // $ remoteFlow
10+
}); // $clientReceive
1111

1212
socket.onmessage = function (event) {
13-
console.log("Message from server 2", event.data)
14-
};
13+
console.log("Message from server 2", event.data); // $ remoteFlow
14+
}; // $clientReceive
1515
})();
1616

1717

1818
(function () {
19-
var sock = new SockJS('http://0.0.0.0:9999/echo');
19+
var sock = new SockJS('http://0.0.0.0:9999/echo'); // $clientSocket
2020
sock.onopen = function () {
21-
sock.send('test');
21+
sock.send('test'); // $clientSend
2222
};
2323

2424
sock.onmessage = function (e) {
25-
console.log('message', e.data);
25+
console.log('message', e.data); // $ remoteFlow
2626
sock.close();
27-
};
27+
}; // $clientReceive
2828

2929
sock.addEventListener('message', function (event) {
30-
console.log('Using addEventListener ', event.data);
31-
});
32-
})
30+
console.log('Using addEventListener ', event.data); // $ remoteFlow
31+
}); // $clientReceive
32+
})();
33+
34+
export const MyWebSocket = WebSocket;
35+
export const MySockJS = SockJS;
36+
export const myWebSocketInstance = new WebSocket('ws://localhost:8080'); // $ clientSocket
37+
export const mySockJSInstance = new SockJS('http://0.0.0.0:9999/echo'); // $ clientSocket
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { MyWebSocketWS, myWebSocketWSInstance } = require('./client.js');
2+
3+
(function () {
4+
const ws = new MyWebSocketWS('ws://example.org'); // $ clientSocket
5+
6+
ws.on('open', function open() {
7+
ws.send('Hi from client!'); // $ clientSend
8+
});
9+
10+
ws.on('message', function incoming(data) { // $ remoteFlow
11+
console.log(data);
12+
}); // $ clientReceive
13+
})();
14+
15+
(function () {
16+
myWebSocketWSInstance.on('open', function open() {
17+
myWebSocketWSInstance.send('Hi from client!'); // $ clientSend
18+
});
19+
20+
myWebSocketWSInstance.on('message', function incoming(data) { // $ remoteFlow
21+
console.log(data);
22+
}); // $ clientReceive
23+
})();
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
(function () {
2-
const WebSocket = require('ws');
1+
const WebSocket = require('ws');
32

4-
const ws = new WebSocket('ws://example.org');
3+
(function () {
4+
const ws = new WebSocket('ws://example.org'); // $clientSocket
55

66
ws.on('open', function open() {
7-
ws.send('Hi from client!');
7+
ws.send('Hi from client!'); // $clientSend
88
});
99

10-
ws.on('message', function incoming(data) {
10+
ws.on('message', function incoming(data) { // $ remoteFlow
1111
console.log(data);
12-
});
13-
})();
12+
}); // $clientReceive
13+
})();
14+
15+
module.exports.MyWebSocketWS = require('ws');
16+
module.exports.myWebSocketWSInstance = new WebSocket('ws://example.org'); // $ clientSocket
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { MyWebSocketServer, myWebSocketServerInstance } = require('./server.js');
2+
3+
(function () {
4+
const wss = new MyWebSocketServer({ port: 8080 });
5+
6+
wss.on('connection', function connection(ws) { // $ serverSocket
7+
ws.on('message', function incoming(message) { // $ remoteFlow
8+
console.log('received: %s', message);
9+
}); // $ serverReceive
10+
11+
ws.send('Hi from server!'); // $ serverSend
12+
});
13+
})();
14+
15+
(function () {
16+
myWebSocketServerInstance.on('connection', function connection(ws) { // $ serverSocket
17+
ws.on('message', function incoming(message) { // $ remoteFlow
18+
console.log('received: %s', message);
19+
}); // $ serverReceive
20+
21+
ws.send('Hi from server!'); // $ serverSend
22+
});
23+
})();

0 commit comments

Comments
 (0)