Skip to content
Open
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
4 changes: 4 additions & 0 deletions python/ql/lib/change-notes/2025-11-22-tornado-websockets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Additional models for remote flow sources for `tornado.websocket.WebSocketHandler` have been added.
59 changes: 59 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Tornado.qll
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@
API::Node subclassRef() {
result = web().getMember("RequestHandler").getASubclass*()
or
result = WebSocket::WebSocketHandler::subclassRef()
or
result = ModelOutput::getATypeNode("tornado.web.RequestHandler~Subclass").getASubclass*()
}

Expand Down Expand Up @@ -428,6 +430,42 @@
}
}
}

// ---------------------------------------------------------------------------
// tornado.websocket
// ---------------------------------------------------------------------------
/** Gets a reference to the `tornado.websocket` module. */
API::Node websocket() { result = Tornado::tornado().getMember("websocket") }

module WebSocket {

Check warning on line 440 in python/ql/lib/semmle/python/frameworks/Tornado.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for module Tornado::Tornado::TornadoModule::WebSocket
module WebSocketHandler {

Check warning on line 441 in python/ql/lib/semmle/python/frameworks/Tornado.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for module Tornado::Tornado::TornadoModule::WebSocket::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 {

Check warning on line 450 in python/ql/lib/semmle/python/frameworks/Tornado.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for class Tornado::Tornado::TornadoModule::WebSocket::WebSocketHandler::WebSocketHandlerClass
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. */
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The term "websocket" should be capitalized as "WebSocket" to maintain consistency with line 583 and adhere to the standard capitalization of the WebSocket protocol name.

Copilot generated this review using guidance from repository custom instructions.
Function getAWebSocketEventHandler() {
result = this.getAMethod() and
result.getName() =
["on_message", "on_close", "on_ping", "on_pong", "select_subprotocol", "check_origin"]
}
}
}
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -542,6 +580,27 @@
override string getFramework() { result = "Tornado" }
}

/** A request handler for WebSocket events */
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The comment should end with a period for consistency with other similar comments in the file, such as the comment on line 560.

Copilot generated this review using guidance from repository custom instructions.
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
// ---------------------------------------------------------------------------
Expand Down
22 changes: 22 additions & 0 deletions python/ql/test/library-tests/frameworks/tornado/routing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ class PossiblyNotRouted(tornado.web.RequestHandler):
def get(self): # $ requestHandler
self.write("NotRouted") # $ HttpResponse

class WebSocket(tornado.websocket.WebSocketHandler):
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The WebSocket class inherits from tornado.websocket.WebSocketHandler on line 57, but the module tornado.websocket is not imported. An import statement should be added at the top of the file, e.g., import tornado.websocket.

Copilot uses AI. Check for mistakes.
def open(self, x): # $ requestHandler routedParameter=x
self.write_message("WebSocket open {}".format(x))
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Trailing whitespace should be removed from this line.

Suggested change
self.write_message("WebSocket open {}".format(x))
self.write_message("WebSocket open {}".format(x))

Copilot uses AI. Check for mistakes.

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



Comment on lines +77 to 78
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

Trailing whitespace should be removed from this blank line.

Suggested change

Copilot uses AI. Check for mistakes.
def make_app():
# see https://www.tornadoweb.org/en/stable/routing.html for even more examples
Expand All @@ -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,
Expand Down
Loading