implement a model of WebSocket and ws based on the EventEmitter model

This commit is contained in:
Erik Krogh Kristensen
2020-01-12 15:42:54 +01:00
parent 007b0795ec
commit b526a2ea0f
7 changed files with 266 additions and 0 deletions

View File

@@ -96,6 +96,7 @@ import semmle.javascript.frameworks.TorrentLibraries
import semmle.javascript.frameworks.Typeahead
import semmle.javascript.frameworks.UriLibraries
import semmle.javascript.frameworks.Vue
import semmle.javascript.frameworks.WebSocket
import semmle.javascript.frameworks.XmlParsers
import semmle.javascript.frameworks.xUnit
import semmle.javascript.linters.ESLint

View File

@@ -0,0 +1,185 @@
/**
* Provides classes for working with [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket).
*
* The model is based on the EventEmitter model, and there a therefore a
* data-flow step from where a WebSocket event is send to where the message
* is received.
*
* WebSockets include no concept of channels, therefore every client can send
* to every server (and vice versa).
*/
import javascript
/**
* Gets the channel name used throughout this WebSocket model.
* WebSockets don't have a concept of channels, and therefore a singleton name is used.
* The name can be anything, as long as it is used consistently in this WebSocket model.
*/
private string channelName() { result = "message" }
module ClientWebSocket {
/**
* A class that can be used to instantiate a WebSocket instance.
*/
class SocketClass extends DataFlow::SourceNode {
boolean isNode;
SocketClass() {
this = DataFlow::globalVarRef("WebSocket") and isNode = false
or
this = DataFlow::moduleImport("ws") and isNode = true
}
/**
* Holds if this class an import of the "ws" module.
*/
predicate isNode() { isNode = true }
}
/**
* A client WebSocket instance.
*/
class ClientSocket extends EventEmitter::Range, DataFlow::SourceNode {
SocketClass socketClass;
ClientSocket() { this = socketClass.getAnInstantiation() }
/**
* Holds if this ClientSocket is created from the "ws" module.
*
* The predicate is used to differentiate where the behavior of the "ws" module differs from the native WebSocket in browsers.
*/
predicate isNode() { socketClass.isNode() }
}
/**
* A message sent from a WebSocket client.
*/
class SendNode extends EventDispatch::Range, DataFlow::CallNode {
override ClientSocket emitter;
SendNode() { this = emitter.getAMemberCall("send") }
override string getChannel() { result = channelName() }
override DataFlow::Node getSentItem(int i) { i = 0 and result = this.getArgument(0) }
override ServerWebSocket::ReceiveNode getAReceiver() { any() }
}
/**
* Gets a methodName that can be used to register a listener for WebSocket messages on a given socket.
*/
string getARegisterMethodName(ClientSocket socket) {
result = "addEventListener"
or
socket.isNode() and
result = EventEmitter::on()
}
/**
* A handler that is registered to receive messages from a WebSocket.
*
* If the registration happens with the "addEventListener" method or the "onmessage" setter property, then the handler receives an event with a "data" property.
* Otherwise the handler receives the data directly.
*
* This confusing API is caused by the "ws" library only mostly using their own API, where event objects are not used.
* But the "ws" library additionally supports the WebSocket API from browsers, which exclusively use event objects with a "data" property.
*/
class ReceiveNode extends EventRegistration::Range, DataFlow::FunctionNode {
override ClientSocket emitter;
boolean receivesEvent;
ReceiveNode() {
exists(DataFlow::CallNode call, string methodName |
methodName = getARegisterMethodName(emitter) and
call = emitter.getAMemberCall(methodName) and
call.getArgument(0).mayHaveStringValue("message") and
this = call.getCallback(1) and
if methodName = "addEventListener" then receivesEvent = true else receivesEvent = false
)
or
this = emitter.getAPropertyWrite("onmessage").getRhs() and
receivesEvent = true
}
override string getChannel() { result = channelName() }
override DataFlow::Node getReceivedItem(int i) {
i = 0 and
result = this.getParameter(0).getAPropertyRead("data") and
receivesEvent = true
or
i = 0 and
result = this.getParameter(0) and
receivesEvent = false
}
}
}
module ServerWebSocket {
/**
* A server WebSocket instance.
*/
class ServerSocket extends EventEmitter::Range, DataFlow::SourceNode {
ServerSocket() {
exists(DataFlow::CallNode onCall |
onCall = DataFlow::moduleImport("ws")
.getAConstructorInvocation("Server")
.getAMemberCall(EventEmitter::on()) and
onCall.getArgument(0).mayHaveStringValue("connection")
|
this = onCall.getCallback(1).getParameter(0)
)
}
}
/**
* A message sent from a WebSocket server.
*/
class SendNode extends EventDispatch::Range, DataFlow::CallNode {
override ServerSocket emitter;
SendNode() { this = emitter.getAMemberCall("send") }
override string getChannel() { result = channelName() }
override DataFlow::Node getSentItem(int i) {
i = 0 and
result = getArgument(0)
}
override ClientWebSocket::ReceiveNode getAReceiver() { any() }
}
/**
* A registration of an event handler that receives data from a WebSocket.
*/
class ReceiveNode extends EventRegistration::Range, DataFlow::CallNode {
override ServerSocket emitter;
ReceiveNode() {
this = emitter.getAMemberCall(EventEmitter::on()) and
this.getArgument(0).mayHaveStringValue("message")
}
override string getChannel() { result = channelName() }
override DataFlow::Node getReceivedItem(int i) {
i = 0 and
result = this.getCallback(1).getParameter(0)
}
}
/**
* A data flow node representing data received from a client, viewed as remote user input.
*/
private class ReceivedItemAsRemoteFlow extends RemoteFlowSource {
ReceivedItemAsRemoteFlow() { this = any(ReceiveNode rercv).getReceivedItem(_) }
override string getSourceType() { result = "WebSocket client data" }
override predicate isUserControlledObject() { any() }
}
}

View File

@@ -0,0 +1,15 @@
(function () {
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', function (event) {
socket.send('Hi from browser!');
});
socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});
socket.onmessage = function(event) {
console.log("Message from server 2", event.data)
};
})();

View File

@@ -0,0 +1,13 @@
(function () {
const WebSocket = require('ws');
const ws = new WebSocket('ws://example.org');
ws.on('open', function open() {
ws.send('Hi from client!');
});
ws.on('message', function incoming(data) {
console.log(data);
});
})();

View File

@@ -0,0 +1,13 @@
(function () {
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('Hi from server!');
});
})();

View File

@@ -0,0 +1,22 @@
clientSocket
| browser.js:2:17:2:52 | new Web ... :8080') |
| client.js:4:13:4:45 | new Web ... e.org') |
clientSend
| browser.js:5:6:5:36 | socket. ... wser!') |
| client.js:7:5:7:30 | ws.send ... ient!') |
clientReceive
| browser.js:8:37:10:2 | functio ... ta);\\n\\t} |
| browser.js:12:21:14:5 | functio ... )\\n } |
| client.js:10:19:12:2 | functio ... ta);\\n\\t} |
serverSocket
| server.js:6:43:6:44 | ws |
serverSend
| server.js:11:5:11:30 | ws.send ... rver!') |
serverReceive
| server.js:7:5:9:6 | ws.on(' ... \\n \\t\\t}) |
taintStep
| browser.js:5:18:5:35 | 'Hi from browser!' | server.js:7:40:7:46 | message |
| client.js:7:13:7:29 | 'Hi from client!' | server.js:7:40:7:46 | message |
| server.js:11:13:11:29 | 'Hi from server!' | browser.js:9:42:9:51 | event.data |
| server.js:11:13:11:29 | 'Hi from server!' | browser.js:13:44:13:53 | event.data |
| server.js:11:13:11:29 | 'Hi from server!' | client.js:10:37:10:40 | data |

View File

@@ -0,0 +1,17 @@
import javascript
query ClientWebSocket::ClientSocket clientSocket() { any() }
query ClientWebSocket::SendNode clientSend() { any() }
query ClientWebSocket::ReceiveNode clientReceive() { any() }
query ServerWebSocket::ServerSocket serverSocket() { any() }
query ServerWebSocket::SendNode serverSend() { any() }
query ServerWebSocket::ReceiveNode serverReceive() { any() }
query predicate taintStep(DataFlow::Node pred, DataFlow::Node succ) {
any(DataFlow::AdditionalFlowStep s).step(pred, succ)
}