Skip to content

JS: Refactor WebSocket to use API graphs #19218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions javascript/ql/lib/change-notes/2025-04-07-websocket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Improved detection of `WebSocket` and `SockJS` usage.
* Added data received from `WebSocket` clients as a remote flow source.
97 changes: 78 additions & 19 deletions javascript/ql/lib/semmle/javascript/frameworks/WebSocket.qll
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ private predicate areLibrariesCompatible(
(client = LibraryNames::ws() or client = LibraryNames::websocket())
}

/** Treats `WebSocket` as an entry point for API graphs. */
private class WebSocketEntryPoint extends API::EntryPoint {
WebSocketEntryPoint() { this = "global.WebSocket" }

override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("WebSocket") }
}

/** Treats `SockJS` as an entry point for API graphs. */
private class SockJSEntryPoint extends API::EntryPoint {
SockJSEntryPoint() { this = "global.SockJS" }

override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("SockJS") }
}

/**
* Provides classes that model WebSockets clients.
*/
Expand All @@ -56,7 +70,7 @@ module ClientWebSocket {
/**
* A class that can be used to instantiate a WebSocket instance.
*/
class SocketClass extends DataFlow::SourceNode {
deprecated class SocketClass extends DataFlow::SourceNode {
LibraryName library; // the name of the WebSocket library. Can be one of the libraries defined in `LibraryNames`.

SocketClass() {
Expand All @@ -78,13 +92,38 @@ module ClientWebSocket {
LibraryName getLibrary() { result = library }
}

/**
* A class that can be used to instantiate a WebSocket instance.
*/
class WebSocketClass extends API::Node {
LibraryName library; // the name of the WebSocket library. Can be one of the libraries defined in `LibraryNames`.

WebSocketClass() {
this = any(WebSocketEntryPoint e).getANode() and library = websocket()
or
this = API::moduleImport("ws") and library = ws()
or
// the sockjs-client library:https://www.npmjs.com/package/sockjs-client
library = sockjs() and
(
this = API::moduleImport("sockjs-client") or
this = any(SockJSEntryPoint e).getANode()
)
}

/**
* Gets the WebSocket library name.
*/
LibraryName getLibrary() { result = library }
}

/**
* A client WebSocket instance.
*/
class ClientSocket extends EventEmitter::Range, DataFlow::NewNode, ClientRequest::Range {
SocketClass socketClass;
class ClientSocket extends EventEmitter::Range, API::NewNode, ClientRequest::Range {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this class won't have to be deprecated, because API::NewNode is a subtype of DataFlow::NewNode, and likewise for API::CallNode.

WebSocketClass socketClass;

ClientSocket() { this = socketClass.getAnInstantiation() }
ClientSocket() { this = socketClass.getAnInvocation() }

/**
* Gets the WebSocket library name.
Expand Down Expand Up @@ -115,10 +154,10 @@ module ClientWebSocket {
/**
* A message sent from a WebSocket client.
*/
class SendNode extends EventDispatch::Range, DataFlow::CallNode {
class SendNode extends EventDispatch::Range, API::CallNode {
override ClientSocket emitter;

SendNode() { this = emitter.getAMemberCall("send") }
SendNode() { this = emitter.getReturn().getMember("send").getACall() }

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

Expand All @@ -145,8 +184,8 @@ module ClientWebSocket {
private DataFlow::FunctionNode getAMessageHandler(
ClientWebSocket::ClientSocket emitter, string methodName
) {
exists(DataFlow::CallNode call |
call = emitter.getAMemberCall(methodName) and
exists(API::CallNode call |
call = emitter.getReturn().getMember(methodName).getACall() and
call.getArgument(0).mayHaveStringValue("message") and
result = call.getCallback(1)
)
Expand All @@ -161,7 +200,13 @@ module ClientWebSocket {
WebSocketReceiveNode() {
this = getAMessageHandler(emitter, "addEventListener")
or
this = emitter.getAPropertyWrite("onmessage").getRhs()
this = emitter.getReturn().getMember("onmessage").getAValueReachingSink()
or
exists(DataFlow::MethodCallNode bindCall |
bindCall = emitter.getReturn().getMember("onmessage").getAValueReachingSink() and
bindCall.getMethodName() = "bind" and
this = bindCall.getReceiver().getAFunctionValue()
)
}

override DataFlow::Node getReceivedItem(int i) {
Expand Down Expand Up @@ -192,19 +237,30 @@ module ServerWebSocket {
/**
* Gets a server created by a library named `library`.
*/
DataFlow::SourceNode getAServer(LibraryName library) {
deprecated DataFlow::SourceNode getAServer(LibraryName library) {
library = ws() and
result = DataFlow::moduleImport("ws").getAConstructorInvocation("Server")
or
library = sockjs() and
result = DataFlow::moduleImport("sockjs").getAMemberCall("createServer")
}

/**
* Gets a server created by a library named `library`.
*/
API::InvokeNode getAServerInvocation(LibraryName library) {
library = ws() and
result = API::moduleImport("ws").getMember("Server").getAnInvocation()
or
library = sockjs() and
result = API::moduleImport("sockjs").getMember("createServer").getAnInvocation()
}

/**
* Gets a `socket.on("connection", (msg, req) => {})` call.
*/
private DataFlow::CallNode getAConnectionCall(LibraryName library) {
result = getAServer(library).getAMemberCall(EventEmitter::on()) and
result = getAServerInvocation(library).getReturn().getMember(EventEmitter::on()).getACall() and
result.getArgument(0).mayHaveStringValue("connection")
}

Expand Down Expand Up @@ -324,15 +380,18 @@ module ServerWebSocket {
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(_) }
/**
* A data flow node representing data received from a client or server, viewed as remote user input.
*/
private class ReceivedItemAsRemoteFlow extends RemoteFlowSource {
ReceivedItemAsRemoteFlow() {
this = any(ClientWebSocket::ReceiveNode rercv).getReceivedItem(_) or
this = any(ServerWebSocket::ReceiveNode rercv).getReceivedItem(_)
}

override string getSourceType() { result = "WebSocket client data" }
override string getSourceType() { result = "WebSocket transmitted data" }

override predicate isUserControlledObject() { any() }
}
override predicate isUserControlledObject() { any() }
}
10 changes: 10 additions & 0 deletions javascript/ql/src/Security/trest/test.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import javascript

API::NewNode getAWebSocketInstance() { result instanceof ClientWebSocket::ClientSocket }

from DataFlow::Node handler
where
handler = getAWebSocketInstance().getReturn().getMember("onmessage").asSource()
or
handler = getAWebSocketInstance().getAPropertyWrite("onmessage").getRhs()
select handler, "This is a WebSocket onmessage handler."
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { MyWebSocket, MySockJS, myWebSocketInstance, mySockJSInstance } from './browser.js';

(function () {
const socket = new MyWebSocket('ws://localhost:9080'); // $ clientSocket

socket.addEventListener('open', function (event) {
socket.send('Hi from browser!'); // $ clientSend
});

socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data); // $ remoteFlow
}); // $ clientReceive

socket.onmessage = function (event) {
console.log("Message from server 2", event.data); // $ remoteFlow
}; // $ clientReceive
})();


(function () {
var sock = new MySockJS('http://0.0.0.0:9999/echo'); // $ clientSocket
sock.onopen = function () {
sock.send('test'); // $ clientSend
};

sock.onmessage = function (e) {
console.log('message', e.data); // $ remoteFlow
sock.close();
}; // $ clientReceive

sock.addEventListener('message', function (event) {
console.log('Using addEventListener ', event.data); // $ remoteFlow
}); // $ clientReceive
})();


(function () {
myWebSocketInstance.addEventListener('open', function (event) {
myWebSocketInstance.send('Hi from browser!'); // $ clientSend
});

myWebSocketInstance.addEventListener('message', function (event) {
console.log('Message from server ', event.data); // $ remoteFlow
}); // $ clientReceive

myWebSocketInstance.onmessage = function (event) {
console.log("Message from server 2", event.data); // $ remoteFlow
}; // $ clientReceive
})();


(function () {
mySockJSInstance.onopen = function () {
mySockJSInstance.send('test'); // $ clientSend
};

mySockJSInstance.onmessage = function (e) {
console.log('message', e.data); // $ remoteFlow
mySockJSInstance.close();
}; // $ clientReceive

mySockJSInstance.addEventListener('message', function (event) {
console.log('Using addEventListener ', event.data); // $ remoteFlow
}); // $ clientReceive
})();


const recv_message = function (e) {
console.log('Received message:', e.data); // $ remoteFlow
}; // $ clientReceive

(function () {
myWebSocketInstance.onmessage = recv_message.bind(this);
})();
31 changes: 18 additions & 13 deletions javascript/ql/test/library-tests/frameworks/WebSocket/browser.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
(function () {
const socket = new WebSocket('ws://localhost:8080');
const socket = new WebSocket('ws://localhost:8080'); // $clientSocket

socket.addEventListener('open', function (event) {
socket.send('Hi from browser!');
socket.send('Hi from browser!'); // $clientSend
});

socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});
console.log('Message from server ', event.data); // $ remoteFlow
}); // $clientReceive

socket.onmessage = function (event) {
console.log("Message from server 2", event.data)
};
console.log("Message from server 2", event.data); // $ remoteFlow
}; // $clientReceive
})();


(function () {
var sock = new SockJS('http://0.0.0.0:9999/echo');
var sock = new SockJS('http://0.0.0.0:9999/echo'); // $clientSocket
sock.onopen = function () {
sock.send('test');
sock.send('test'); // $clientSend
};

sock.onmessage = function (e) {
console.log('message', e.data);
console.log('message', e.data); // $ remoteFlow
sock.close();
};
}; // $clientReceive

sock.addEventListener('message', function (event) {
console.log('Using addEventListener ', event.data);
});
})
console.log('Using addEventListener ', event.data); // $ remoteFlow
}); // $clientReceive
})();

export const MyWebSocket = WebSocket;
export const MySockJS = SockJS;
export const myWebSocketInstance = new WebSocket('ws://localhost:8080'); // $ clientSocket
export const mySockJSInstance = new SockJS('http://0.0.0.0:9999/echo'); // $ clientSocket
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const { MyWebSocketWS, myWebSocketWSInstance } = require('./client.js');

(function () {
const ws = new MyWebSocketWS('ws://example.org'); // $ clientSocket

ws.on('open', function open() {
ws.send('Hi from client!'); // $ clientSend
});

ws.on('message', function incoming(data) { // $ remoteFlow
console.log(data);
}); // $ clientReceive
})();

(function () {
myWebSocketWSInstance.on('open', function open() {
myWebSocketWSInstance.send('Hi from client!'); // $ clientSend
});

myWebSocketWSInstance.on('message', function incoming(data) { // $ remoteFlow
console.log(data);
}); // $ clientReceive
})();
17 changes: 10 additions & 7 deletions javascript/ql/test/library-tests/frameworks/WebSocket/client.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
(function () {
const WebSocket = require('ws');
const WebSocket = require('ws');

const ws = new WebSocket('ws://example.org');
(function () {
const ws = new WebSocket('ws://example.org'); // $clientSocket

ws.on('open', function open() {
ws.send('Hi from client!');
ws.send('Hi from client!'); // $clientSend
});

ws.on('message', function incoming(data) {
ws.on('message', function incoming(data) { // $ remoteFlow
console.log(data);
});
})();
}); // $clientReceive
})();

module.exports.MyWebSocketWS = require('ws');
module.exports.myWebSocketWSInstance = new WebSocket('ws://example.org'); // $ clientSocket
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const { MyWebSocketServer, myWebSocketServerInstance } = require('./server.js');

(function () {
const wss = new MyWebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) { // $ serverSocket
ws.on('message', function incoming(message) { // $ remoteFlow
console.log('received: %s', message);
}); // $ serverReceive

ws.send('Hi from server!'); // $ serverSend
});
})();

(function () {
myWebSocketServerInstance.on('connection', function connection(ws) { // $ serverSocket
ws.on('message', function incoming(message) { // $ remoteFlow
console.log('received: %s', message);
}); // $ serverReceive

ws.send('Hi from server!'); // $ serverSend
});
})();
Loading