From cdc44c326769ce346dd3e3b828a63facaf9032a1 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 14 Nov 2025 09:58:52 +0000 Subject: [PATCH 1/4] Model tornado websockets --- .../lib/semmle/python/frameworks/Tornado.qll | 59 +++++++++++++++++++ .../frameworks/tornado/routing_test.py | 22 +++++++ 2 files changed, 81 insertions(+) diff --git a/python/ql/lib/semmle/python/frameworks/Tornado.qll b/python/ql/lib/semmle/python/frameworks/Tornado.qll index 7cfe381b1f9..a6ed8292dd7 100644 --- a/python/ql/lib/semmle/python/frameworks/Tornado.qll +++ b/python/ql/lib/semmle/python/frameworks/Tornado.qll @@ -135,6 +135,8 @@ module Tornado { API::Node subclassRef() { result = web().getMember("RequestHandler").getASubclass*() or + result = WebSocket::WebSocketHandler::subclassRef() + or result = ModelOutput::getATypeNode("tornado.web.RequestHandler~Subclass").getASubclass*() } @@ -428,6 +430,42 @@ module Tornado { } } } + + // --------------------------------------------------------------------------- + // tornado.websocket + // --------------------------------------------------------------------------- + /** Gets a reference to the `tornado.websocket` module. */ + API::Node websocket() { result = Tornado::tornado().getMember("websocket") } + + module WebSocket { + module WebSocketHandler { + /** Gets a reference to the `tornado.websocket.WebSocketHandler` class or any subclass. */ + API::Node subclassRef() { + result = websocket().getMember("WebSocketHandler").getASubclass*() + or + result = + ModelOutput::getATypeNode("tornado.websocket.WebSocketHandler~Subclass").getASubclass*() + } + + class WebSocketHandlerClass extends Web::RequestHandler::RequestHandlerClass { + WebSocketHandlerClass() { this.getParent() = subclassRef().asSource().asExpr() } + + override Function getARequestHandler() { + result = super.getARequestHandler() + or + result = this.getAMethod() and + result.getName() = "open" + } + + /** Gets a function that could handle incoming websocket events, if any. */ + Function getAWebSocketEventHandler() { + result = this.getAMethod() and + result.getName() = + ["on_message", "on_close", "on_ping", "on_pong", "select_subprotocol", "check_origin"] + } + } + } + } } // --------------------------------------------------------------------------- @@ -542,6 +580,27 @@ module Tornado { override string getFramework() { result = "Tornado" } } + /** A request handler for WebSocket events */ + private class TornadoWebSocketEventHandler extends Http::Server::RequestHandler::Range { + TornadoWebSocketEventHandler() { + exists(TornadoModule::WebSocket::WebSocketHandler::WebSocketHandlerClass cls | + cls.getAWebSocketEventHandler() = this + ) + } + + override Parameter getARoutedParameter() { + // The `open` method is handled as a normal request handler in `TornadoRouteSetup` or `TornadoRequestHandlerWithoutKnownRoute`. + // For other event handlers (such as `on_message`), all parameters should be remote flow sources, as they are not affected by routing. + result in [ + this.getArg(_), this.getArgByName(_), this.getVararg().(Parameter), + this.getKwarg().(Parameter) + ] and + not result = this.getArg(0) + } + + override string getFramework() { result = "Tornado" } + } + // --------------------------------------------------------------------------- // Response modeling // --------------------------------------------------------------------------- diff --git a/python/ql/test/library-tests/frameworks/tornado/routing_test.py b/python/ql/test/library-tests/frameworks/tornado/routing_test.py index 2b596c20ce5..931be7b4bb6 100644 --- a/python/ql/test/library-tests/frameworks/tornado/routing_test.py +++ b/python/ql/test/library-tests/frameworks/tornado/routing_test.py @@ -54,6 +54,27 @@ class PossiblyNotRouted(tornado.web.RequestHandler): def get(self): # $ requestHandler self.write("NotRouted") # $ HttpResponse +class WebSocket(tornado.websocket.WebSocketHandler): + def open(self, x): # $ requestHandler routedParameter=x + self.write_message("WebSocket open {}".format(x)) + + def on_message(self, data): # $ requestHandler routedParameter=data + self.write_message("WebSocket on_message {}".format(data)) + + def on_ping(self, data): # $ requestHandler routedParameter=data + print("ping", data) + + def on_pong(self, data): # $ requestHandler routedParameter=data + print("pong", data) + + def select_subprotocol(self, subs): # $ requestHandler routedParameter=subs + print("select_subprotocol", subs) + + def check_origin(self, origin): # $ requestHandler routedParameter=origin + print("check_origin", origin) + return True + + def make_app(): # see https://www.tornadoweb.org/en/stable/routing.html for even more examples @@ -74,6 +95,7 @@ def make_app(): (tornado.routing.HostMatches(r"(localhost|127\.0\.0\.1)"), [ ("/only-localhost", OnlyLocalhost) # $ routeSetup="/only-localhost" ]), + (r"/websocket/([0-9]+)", WebSocket), # $ routeSetup="/websocket/([0-9]+)" ], debug=True, From 9c3f4e2bfb990d023f3dc1ec4c3a33d7a0c819e4 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 20 Nov 2025 10:59:05 +0000 Subject: [PATCH 2/4] Add changenote --- python/ql/lib/change-notes/2025-11-22-tornado-websockets.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 python/ql/lib/change-notes/2025-11-22-tornado-websockets.md diff --git a/python/ql/lib/change-notes/2025-11-22-tornado-websockets.md b/python/ql/lib/change-notes/2025-11-22-tornado-websockets.md new file mode 100644 index 00000000000..8ba2ef549ee --- /dev/null +++ b/python/ql/lib/change-notes/2025-11-22-tornado-websockets.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Additional models for remote flow sources for `tornado.websocket.WebSocketHandler` have been added. \ No newline at end of file From dada49f402c9d9e1843707e5acf5ed8d1c8a2f89 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 24 Nov 2025 13:57:43 +0000 Subject: [PATCH 3/4] Fix qldoc and tests --- python/ql/lib/semmle/python/frameworks/Tornado.qll | 11 +++++++++-- .../library-tests/frameworks/tornado/routing_test.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/Tornado.qll b/python/ql/lib/semmle/python/frameworks/Tornado.qll index a6ed8292dd7..61cf7df316e 100644 --- a/python/ql/lib/semmle/python/frameworks/Tornado.qll +++ b/python/ql/lib/semmle/python/frameworks/Tornado.qll @@ -437,7 +437,13 @@ module Tornado { /** Gets a reference to the `tornado.websocket` module. */ API::Node websocket() { result = Tornado::tornado().getMember("websocket") } + /** Provides models for the `tornado.websocket` module */ module WebSocket { + /** + * Provides models for the `tornado.websocket.WebSocketHandler` class and subclasses. + * + * See https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler. + */ module WebSocketHandler { /** Gets a reference to the `tornado.websocket.WebSocketHandler` class or any subclass. */ API::Node subclassRef() { @@ -447,6 +453,7 @@ module Tornado { ModelOutput::getATypeNode("tornado.websocket.WebSocketHandler~Subclass").getASubclass*() } + /** A subclass of `tornado.websocket.WebSocketHandler`. */ class WebSocketHandlerClass extends Web::RequestHandler::RequestHandlerClass { WebSocketHandlerClass() { this.getParent() = subclassRef().asSource().asExpr() } @@ -457,7 +464,7 @@ module Tornado { result.getName() = "open" } - /** Gets a function that could handle incoming websocket events, if any. */ + /** Gets a function that could handle incoming WebSocket events, if any. */ Function getAWebSocketEventHandler() { result = this.getAMethod() and result.getName() = @@ -580,7 +587,7 @@ module Tornado { override string getFramework() { result = "Tornado" } } - /** A request handler for WebSocket events */ + /** A request handler for WebSocket events. */ private class TornadoWebSocketEventHandler extends Http::Server::RequestHandler::Range { TornadoWebSocketEventHandler() { exists(TornadoModule::WebSocket::WebSocketHandler::WebSocketHandlerClass cls | diff --git a/python/ql/test/library-tests/frameworks/tornado/routing_test.py b/python/ql/test/library-tests/frameworks/tornado/routing_test.py index 931be7b4bb6..94824e6e862 100644 --- a/python/ql/test/library-tests/frameworks/tornado/routing_test.py +++ b/python/ql/test/library-tests/frameworks/tornado/routing_test.py @@ -1,5 +1,6 @@ import tornado.web import tornado.routing +import tornado.websocket class FooHandler(tornado.web.RequestHandler): @@ -56,7 +57,7 @@ class PossiblyNotRouted(tornado.web.RequestHandler): class WebSocket(tornado.websocket.WebSocketHandler): def open(self, x): # $ requestHandler routedParameter=x - self.write_message("WebSocket open {}".format(x)) + self.write_message("WebSocket open {}".format(x)) def on_message(self, data): # $ requestHandler routedParameter=data self.write_message("WebSocket on_message {}".format(data)) @@ -74,7 +75,6 @@ class WebSocket(tornado.websocket.WebSocketHandler): print("check_origin", origin) return True - def make_app(): # see https://www.tornadoweb.org/en/stable/routing.html for even more examples From 7cf3964e44a0c5b2de48853f60d857335f08e104 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 1 Dec 2025 20:27:48 +0000 Subject: [PATCH 4/4] Update expectations --- .../ql/test/library-tests/frameworks/tornado/routing_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ql/test/library-tests/frameworks/tornado/routing_test.py b/python/ql/test/library-tests/frameworks/tornado/routing_test.py index 94824e6e862..1cff63921da 100644 --- a/python/ql/test/library-tests/frameworks/tornado/routing_test.py +++ b/python/ql/test/library-tests/frameworks/tornado/routing_test.py @@ -57,10 +57,10 @@ class PossiblyNotRouted(tornado.web.RequestHandler): class WebSocket(tornado.websocket.WebSocketHandler): def open(self, x): # $ requestHandler routedParameter=x - self.write_message("WebSocket open {}".format(x)) + self.write_message("WebSocket open {}".format(x)) # $ MISSING: HttpResponse def on_message(self, data): # $ requestHandler routedParameter=data - self.write_message("WebSocket on_message {}".format(data)) + self.write_message("WebSocket on_message {}".format(data)) # $ MISSING: HttpResponse def on_ping(self, data): # $ requestHandler routedParameter=data print("ping", data)