diff --git a/change-notes/1.24/analysis-javascript.md b/change-notes/1.24/analysis-javascript.md index 798e0e59e59..d367d235a8c 100644 --- a/change-notes/1.24/analysis-javascript.md +++ b/change-notes/1.24/analysis-javascript.md @@ -16,6 +16,8 @@ - [Electron](https://electronjs.org/) - [Node.js](https://nodejs.org/) - [Socket.IO](https://socket.io/) + - [ws](https://github.com/websockets/ws) + - [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) ## New queries diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index 0c41c62480a..00ec17dc70b 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -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 diff --git a/javascript/ql/src/semmle/javascript/frameworks/WebSocket.qll b/javascript/ql/src/semmle/javascript/frameworks/WebSocket.qll new file mode 100644 index 00000000000..ffe05017057 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/frameworks/WebSocket.qll @@ -0,0 +1,194 @@ +/** + * Provides classes for working with [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [ws](https://github.com/websockets/ws). + * + * The model is based on the EventEmitter model, and there is therefore a + * data-flow step from where a WebSocket event is sent to where the message + * is received. + * + * Data flow is modeled both from clients to servers, and from servers to clients. + * The model models that clients can send messages to all servers, and servers can send messages to all clients. + */ + +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" } + +/** + * Provides classes that model WebSockets clients. + */ +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 is 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() } + } + + /** + * A handler that is registered to receive messages from a WebSocket. + */ + abstract class ReceiveNode extends EventRegistration::Range, DataFlow::FunctionNode { + override ClientSocket emitter; + + override string getChannel() { result = channelName() } + } + + /** + * Gets a handler, that is registered using method `methodName` and receives messages sent to `emitter`. + */ + private DataFlow::FunctionNode getAMessageHandler(ClientWebSocket::ClientSocket emitter, string methodName) { + exists(DataFlow::CallNode call | + call = emitter.getAMemberCall(methodName) and + call.getArgument(0).mayHaveStringValue("message") and + result = call.getCallback(1) + ) + } + + /** + * A handler that receives a message using the WebSocket API. + * The WebSocket API is used both by the WebSocket library in browsers, and the same API is also implemented as part of the "ws" library. + * This class therefore models both the WebSocket library, and a subset of the "ws" library. + */ + private class WebSocketReceiveNode extends ClientWebSocket::ReceiveNode { + WebSocketReceiveNode() { + this = getAMessageHandler(emitter, "addEventListener") + or + this = emitter.getAPropertyWrite("onmessage").getRhs() + } + + override DataFlow::Node getReceivedItem(int i) { + i = 0 and result = this.getParameter(0).getAPropertyRead("data") + } + } + + /** + * A handler that receives a message using the API from the "ws" library. + * The "ws" library additionally implements the WebSocket API, which is modeled in the `WebSocketReceiveNode` class. + */ + private class WSReceiveNode extends ClientWebSocket::ReceiveNode { + WSReceiveNode () { + emitter.isNode() and + this = getAMessageHandler(emitter, EventEmitter::on()) + } + + override DataFlow::Node getReceivedItem(int i) { + i = 0 and result = this.getParameter(0) + } + } +} + +/** + * Provides classes that model WebSocket servers. + */ +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() } + } +} diff --git a/javascript/ql/test/library-tests/frameworks/WebSocket/browser.js b/javascript/ql/test/library-tests/frameworks/WebSocket/browser.js new file mode 100644 index 00000000000..a01e43d56fa --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/WebSocket/browser.js @@ -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) + }; +})(); \ No newline at end of file diff --git a/javascript/ql/test/library-tests/frameworks/WebSocket/client.js b/javascript/ql/test/library-tests/frameworks/WebSocket/client.js new file mode 100644 index 00000000000..d6b8b8868bf --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/WebSocket/client.js @@ -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); + }); +})(); \ No newline at end of file diff --git a/javascript/ql/test/library-tests/frameworks/WebSocket/server.js b/javascript/ql/test/library-tests/frameworks/WebSocket/server.js new file mode 100644 index 00000000000..cec28594088 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/WebSocket/server.js @@ -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!'); + }); +})(); \ No newline at end of file diff --git a/javascript/ql/test/library-tests/frameworks/WebSocket/test.expected b/javascript/ql/test/library-tests/frameworks/WebSocket/test.expected new file mode 100644 index 00000000000..6ba39bdd868 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/WebSocket/test.expected @@ -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 | diff --git a/javascript/ql/test/library-tests/frameworks/WebSocket/test.ql b/javascript/ql/test/library-tests/frameworks/WebSocket/test.ql new file mode 100644 index 00000000000..5cb76ca7ec2 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/WebSocket/test.ql @@ -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) +} \ No newline at end of file