From ba069902903407ce360a1fd92376136ecd7effcc Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 20 Nov 2025 10:47:41 +0000 Subject: [PATCH 1/8] Add socketio models --- .../lib/semmle/python/frameworks/Socketio.qll | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 python/ql/lib/semmle/python/frameworks/Socketio.qll diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll new file mode 100644 index 000000000000..e3680cfae7ff --- /dev/null +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -0,0 +1,59 @@ +/** + * Provides definitions and modelling for the `python-socketio` PyPI package. + * See https://python-socketio.readthedocs.io/en/stable/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.Concepts +private import semmle.python.ApiGraphs +private import semmle.python.frameworks.internal.PoorMansFunctionResolution + +/** + * Provides models for the `python-socketio` PyPI package. + * See https://python-socketio.readthedocs.io/en/stable/. + */ +module SocketIO { + /** An instance of a socketio `Server` or `AsyncServer`. */ + API::Node server() { + result = API::moduleImport("socketio").getMember(["Server", "AsyncServer"]).getAnInstance() + } + + API::Node serverEventAnnotation() { + result = server().getMember("event") + or + result = server().getMember("on").getReturn() + } + + private class EventHandler extends Http::Server::RequestHandler::Range { + EventHandler() { + serverEventAnnotation().getAValueReachableFromSource().asExpr() = this.getADecorator() + } + + override Parameter getARoutedParameter() { result = this.getAnArg() } + + override string getFramework() { result = "socketio" } + } + + private class CallbackArgument extends DataFlow::Node { + CallbackArgument() { + exists(DataFlow::CallCfgNode c | c = server().getMember(["emit", "send"]).getACall() | + this = c.getArgByName("callback") + ) + or + exists(DataFlow::CallCfgNode c | c = server().getMember("on").getACall() | + this = c.getArg(1) or + this = c.getArgByName("handler") + ) + } + } + + private class CallbackHandler extends Http::Server::RequestHandler::Range { + CallbackHandler() { any(CallbackArgument ca) = poorMansFunctionTracker(this) } + + override Parameter getARoutedParameter() { result = this.getAnArg() } + + override string getFramework() { result = "socketio" } + } +} From a83c70f99d8d4c5f17b2d7ed9affdb3efbca0253 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 24 Nov 2025 11:03:16 +0000 Subject: [PATCH 2/8] Add tests --- python/ql/lib/semmle/python/Frameworks.qll | 1 + .../lib/semmle/python/frameworks/Socketio.qll | 15 ++++++++-- .../frameworks/socketio/ConceptsTest.expected | 0 .../frameworks/socketio/ConceptsTest.ql | 2 ++ .../library-tests/frameworks/socketio/test.py | 29 +++++++++++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 python/ql/test/library-tests/frameworks/socketio/ConceptsTest.expected create mode 100644 python/ql/test/library-tests/frameworks/socketio/ConceptsTest.ql create mode 100644 python/ql/test/library-tests/frameworks/socketio/test.py diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll index 955385141f7f..7694419b41d5 100644 --- a/python/ql/lib/semmle/python/Frameworks.qll +++ b/python/ql/lib/semmle/python/Frameworks.qll @@ -78,6 +78,7 @@ private import semmle.python.frameworks.Sanic private import semmle.python.frameworks.ServerLess private import semmle.python.frameworks.Setuptools private import semmle.python.frameworks.Simplejson +private import semmle.python.frameworks.Socketio private import semmle.python.frameworks.SqlAlchemy private import semmle.python.frameworks.Starlette private import semmle.python.frameworks.Stdlib diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll index e3680cfae7ff..973bb9552f7d 100644 --- a/python/ql/lib/semmle/python/frameworks/Socketio.qll +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -6,6 +6,7 @@ private import python private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.Concepts private import semmle.python.ApiGraphs private import semmle.python.frameworks.internal.PoorMansFunctionResolution @@ -31,7 +32,9 @@ module SocketIO { serverEventAnnotation().getAValueReachableFromSource().asExpr() = this.getADecorator() } - override Parameter getARoutedParameter() { result = this.getAnArg() } + override Parameter getARoutedParameter() { + result = this.getAnArg() and not result = this.getArg(0) + } override string getFramework() { result = "socketio" } } @@ -52,8 +55,16 @@ module SocketIO { private class CallbackHandler extends Http::Server::RequestHandler::Range { CallbackHandler() { any(CallbackArgument ca) = poorMansFunctionTracker(this) } - override Parameter getARoutedParameter() { result = this.getAnArg() } + override Parameter getARoutedParameter() { + result = this.getAnArg() and not result = this.getArg(0) + } override string getFramework() { result = "socketio" } } + + private class SocketIOCall extends RemoteFlowSource::Range { + SocketIOCall() { this = server().getMember("call").getACall() } + + override string getSourceType() { result = "socketio call" } + } } diff --git a/python/ql/test/library-tests/frameworks/socketio/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/socketio/ConceptsTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/frameworks/socketio/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/socketio/ConceptsTest.ql new file mode 100644 index 000000000000..b557a0bccb69 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/socketio/ConceptsTest.ql @@ -0,0 +1,2 @@ +import python +import experimental.meta.ConceptsTest diff --git a/python/ql/test/library-tests/frameworks/socketio/test.py b/python/ql/test/library-tests/frameworks/socketio/test.py new file mode 100644 index 000000000000..f603edd3111f --- /dev/null +++ b/python/ql/test/library-tests/frameworks/socketio/test.py @@ -0,0 +1,29 @@ +import socketio + +sio = socketio.Server() + +@sio.on("connect") +def connect(sid, environ, auth): # $ requestHandler routedParameter=environ routedParameter=auth + print("connect", sid, environ, auth) + +@sio.on("event1") +def handle(sid, data): # $ requestHandler routedParameter=data + print("e1", sid, data) + +@sio.event +def event2(sid, data): # $ requestHandler routedParameter=data + print("e2", sid, data) + +def event3(sid, data): # $ requestHandler routedParameter=data + print("e3", sid, data) + +sio.on("event3", handler=event3) + +sio.on("event4", lambda sid,data: print("e4", sid, data)) # $ requestHandler routedParameter=data + + + +if __name__ == "__main__": + app = socketio.WSGIApp(sio) + import eventlet + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) \ No newline at end of file From b0be8184ac164ff0056f9cbd2de5ef2326ab3365 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Mon, 24 Nov 2025 16:54:21 +0000 Subject: [PATCH 3/8] Add taint test --- .../lib/semmle/python/frameworks/Socketio.qll | 18 +++---- .../socketio/InlineTaintTest.expected | 3 ++ .../frameworks/socketio/InlineTaintTest.ql | 2 + .../frameworks/socketio/taint_test.py | 48 +++++++++++++++++++ .../library-tests/frameworks/socketio/test.py | 10 ++-- 5 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.expected create mode 100644 python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.ql create mode 100644 python/ql/test/library-tests/frameworks/socketio/taint_test.py diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll index 973bb9552f7d..6f3dee722788 100644 --- a/python/ql/lib/semmle/python/frameworks/Socketio.qll +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -30,6 +30,15 @@ module SocketIO { private class EventHandler extends Http::Server::RequestHandler::Range { EventHandler() { serverEventAnnotation().getAValueReachableFromSource().asExpr() = this.getADecorator() + or + exists(DataFlow::CallCfgNode c, DataFlow::Node arg | c = server().getMember("on").getACall() | + ( + arg = c.getArg(1) + or + arg = c.getArgByName("handler") + ) and + poorMansFunctionTracker(this) = arg + ) } override Parameter getARoutedParameter() { @@ -44,20 +53,13 @@ module SocketIO { exists(DataFlow::CallCfgNode c | c = server().getMember(["emit", "send"]).getACall() | this = c.getArgByName("callback") ) - or - exists(DataFlow::CallCfgNode c | c = server().getMember("on").getACall() | - this = c.getArg(1) or - this = c.getArgByName("handler") - ) } } private class CallbackHandler extends Http::Server::RequestHandler::Range { CallbackHandler() { any(CallbackArgument ca) = poorMansFunctionTracker(this) } - override Parameter getARoutedParameter() { - result = this.getAnArg() and not result = this.getArg(0) - } + override Parameter getARoutedParameter() { result = this.getAnArg() } override string getFramework() { result = "socketio" } } diff --git a/python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.expected b/python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.expected new file mode 100644 index 000000000000..020c338fd192 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.expected @@ -0,0 +1,3 @@ +argumentToEnsureNotTaintedNotMarkedAsSpurious +untaintedArgumentToEnsureTaintedNotMarkedAsMissing +testFailures diff --git a/python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.ql b/python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.ql new file mode 100644 index 000000000000..8524da5fe7db --- /dev/null +++ b/python/ql/test/library-tests/frameworks/socketio/InlineTaintTest.ql @@ -0,0 +1,2 @@ +import experimental.meta.InlineTaintTest +import MakeInlineTaintTest diff --git a/python/ql/test/library-tests/frameworks/socketio/taint_test.py b/python/ql/test/library-tests/frameworks/socketio/taint_test.py new file mode 100644 index 000000000000..3e635308d404 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/socketio/taint_test.py @@ -0,0 +1,48 @@ +import sys +import socketio +import sys + +def ensure_tainted(*args): + print("tainted", args) + +def ensure_not_tainted(*args): + print("not tainted", args) + +sio = socketio.Server() + +@sio.event +def connect(sid, environ, auth): # $ requestHandler routedParameter=sid routedParameter=environ routedParameter=auth + ensure_not_tainted(sid) + ensure_tainted(environ, # $ tainted + auth) # $ tainted + +@sio.event +def event1(sid, data): # $ requestHandler routedParameter=sid routedParameter=data + ensure_not_tainted(sid) + ensure_tainted(data) # $ tainted + res = sio.call("e1", sid=sid) + ensure_tainted(res) # $ tainted + sio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + sio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + +asio = socketio.AsyncServer(async_mode='asgi') + +@asio.event +async def event2(sid, data): # $ requestHandler routedParameter=sid routedParameter=data + ensure_not_tainted(sid) + ensure_tainted(data) # $ tainted + res = await asio.call("e2", sid=sid) + ensure_tainted(res) # $ tainted + await asio.emit("e3", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + await asio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + +if __name__ == "__main__": + + if "--async" in sys.argv: + import uvicorn + app = socketio.ASGIApp(asio) + uvicorn.run(app, host='127.0.0.1', port=8000) + else: + import eventlet + app = socketio.WSGIApp(sio) + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) \ No newline at end of file diff --git a/python/ql/test/library-tests/frameworks/socketio/test.py b/python/ql/test/library-tests/frameworks/socketio/test.py index f603edd3111f..c511316d51a8 100644 --- a/python/ql/test/library-tests/frameworks/socketio/test.py +++ b/python/ql/test/library-tests/frameworks/socketio/test.py @@ -3,23 +3,23 @@ sio = socketio.Server() @sio.on("connect") -def connect(sid, environ, auth): # $ requestHandler routedParameter=environ routedParameter=auth +def connect(sid, environ, auth): # $ requestHandler routedParameter=sid routedParameter=environ routedParameter=auth print("connect", sid, environ, auth) @sio.on("event1") -def handle(sid, data): # $ requestHandler routedParameter=data +def handle(sid, data): # $ requestHandler routedParameter=sid routedParameter=data print("e1", sid, data) @sio.event -def event2(sid, data): # $ requestHandler routedParameter=data +def event2(sid, data): # $ requestHandler routedParameter=sid routedParameter=data print("e2", sid, data) -def event3(sid, data): # $ requestHandler routedParameter=data +def event3(sid, data): # $ requestHandler routedParameter=sid routedParameter=data print("e3", sid, data) sio.on("event3", handler=event3) -sio.on("event4", lambda sid,data: print("e4", sid, data)) # $ requestHandler routedParameter=data +sio.on("event4", lambda sid,data: print("e4", sid, data)) # $ requestHandler routedParameter=sid routedParameter=data From 83eadbad605baf4f75f5d1345214bf7d9d6da6d4 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Tue, 25 Nov 2025 16:56:36 +0000 Subject: [PATCH 4/8] Add namespace models --- .../lib/semmle/python/frameworks/Socketio.qll | 120 ++++++++++++------ .../frameworks/socketio/taint_test.py | 21 ++- .../library-tests/frameworks/socketio/test.py | 10 +- 3 files changed, 103 insertions(+), 48 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll index 6f3dee722788..ca873d8ecf22 100644 --- a/python/ql/lib/semmle/python/frameworks/Socketio.qll +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -16,57 +16,101 @@ private import semmle.python.frameworks.internal.PoorMansFunctionResolution * See https://python-socketio.readthedocs.io/en/stable/. */ module SocketIO { - /** An instance of a socketio `Server` or `AsyncServer`. */ - API::Node server() { - result = API::moduleImport("socketio").getMember(["Server", "AsyncServer"]).getAnInstance() - } - - API::Node serverEventAnnotation() { - result = server().getMember("event") - or - result = server().getMember("on").getReturn() - } + /** Provides models for socketio `Server` and `AsyncServer` classes. */ + module Server { + /** An instance of a socketio `Server` or `AsyncServer`. */ + API::Node server() { + result = API::moduleImport("socketio").getMember(["Server", "AsyncServer"]).getAnInstance() + } - private class EventHandler extends Http::Server::RequestHandler::Range { - EventHandler() { - serverEventAnnotation().getAValueReachableFromSource().asExpr() = this.getADecorator() + /** A decorator that indicates a socketio event handler. */ + private API::Node serverEventAnnotation() { + result = server().getMember("event") or - exists(DataFlow::CallCfgNode c, DataFlow::Node arg | c = server().getMember("on").getACall() | - ( - arg = c.getArg(1) - or - arg = c.getArgByName("handler") - ) and - poorMansFunctionTracker(this) = arg - ) + result = server().getMember("on").getReturn() + } + + private class EventHandler extends Http::Server::RequestHandler::Range { + EventHandler() { + serverEventAnnotation().getAValueReachableFromSource().asExpr() = this.getADecorator() + or + exists(DataFlow::CallCfgNode c, DataFlow::Node arg | + c = server().getMember("on").getACall() + | + ( + arg = c.getArg(1) + or + arg = c.getArgByName("handler") + ) and + poorMansFunctionTracker(this) = arg + ) + } + + override Parameter getARoutedParameter() { + result = this.getAnArg() and + not result = this.getArg(0) // First parameter is `sid`, which is not a remote flow source as it cannot be controlled by the client. + } + + override string getFramework() { result = "socketio" } } - override Parameter getARoutedParameter() { - result = this.getAnArg() and not result = this.getArg(0) + private class CallbackArgument extends DataFlow::Node { + CallbackArgument() { + exists(DataFlow::CallCfgNode c | + c = [server(), Namespace::instance()].getMember(["emit", "send"]).getACall() + | + this = c.getArgByName("callback") + ) + } } - override string getFramework() { result = "socketio" } - } + private class CallbackHandler extends Http::Server::RequestHandler::Range { + CallbackHandler() { any(CallbackArgument ca) = poorMansFunctionTracker(this) } + + override Parameter getARoutedParameter() { result = this.getAnArg() } - private class CallbackArgument extends DataFlow::Node { - CallbackArgument() { - exists(DataFlow::CallCfgNode c | c = server().getMember(["emit", "send"]).getACall() | - this = c.getArgByName("callback") - ) + override string getFramework() { result = "socketio" } + } + + private class SocketIOCall extends RemoteFlowSource::Range { + SocketIOCall() { this = [server(), Namespace::instance()].getMember("call").getACall() } + + override string getSourceType() { result = "socketio call" } } } - private class CallbackHandler extends Http::Server::RequestHandler::Range { - CallbackHandler() { any(CallbackArgument ca) = poorMansFunctionTracker(this) } + /** Provides modelling for socketio server Namespace/AsyncNamespace classes. */ + module Namespace { + /** Gets a reference to the `socketio.Namespace` or `socketio.AsyncNamespace` classes or any subclass. */ + API::Node subclassRef() { + result = + API::moduleImport("socketio").getMember(["Namespace", "AsyncNamespace"]).getASubclass*() + } - override Parameter getARoutedParameter() { result = this.getAnArg() } + /** Gets a reference to an instance of a subclass of `socketio.Namespace` or `socketio.AsyncNamespace`. */ + API::Node instance() { result = subclassRef().getAnInstance() } - override string getFramework() { result = "socketio" } - } + /** A socketio Namespace class. */ + class NamespaceClass extends Class { + NamespaceClass() { this.getABase() = subclassRef().asSource().asExpr() } - private class SocketIOCall extends RemoteFlowSource::Range { - SocketIOCall() { this = server().getMember("call").getACall() } + /** Gets a handler for socketio events. */ + Function getAnEventHandler() { + result = this.getAMethod() and + result.getName().matches("on_%") + } + } + + private class NamespaceEventHandler extends Http::Server::RequestHandler::Range { + NamespaceEventHandler() { this = any(NamespaceClass nc).getAnEventHandler() } - override string getSourceType() { result = "socketio call" } + override Parameter getARoutedParameter() { + result = this.getAnArg() and + not result = this.getArg(0) and + not result = this.getArg(1) // First 2 parameters are `self` and `sid`. + } + + override string getFramework() { result = "socketio" } + } } } diff --git a/python/ql/test/library-tests/frameworks/socketio/taint_test.py b/python/ql/test/library-tests/frameworks/socketio/taint_test.py index 3e635308d404..0073e36ff60a 100644 --- a/python/ql/test/library-tests/frameworks/socketio/taint_test.py +++ b/python/ql/test/library-tests/frameworks/socketio/taint_test.py @@ -11,13 +11,13 @@ def ensure_not_tainted(*args): sio = socketio.Server() @sio.event -def connect(sid, environ, auth): # $ requestHandler routedParameter=sid routedParameter=environ routedParameter=auth +def connect(sid, environ, auth): # $ requestHandler routedParameter=environ routedParameter=auth ensure_not_tainted(sid) ensure_tainted(environ, # $ tainted auth) # $ tainted @sio.event -def event1(sid, data): # $ requestHandler routedParameter=sid routedParameter=data +def event1(sid, data): # $ requestHandler routedParameter=data ensure_not_tainted(sid) ensure_tainted(data) # $ tainted res = sio.call("e1", sid=sid) @@ -25,15 +25,26 @@ def event1(sid, data): # $ requestHandler routedParameter=sid routedParameter=da sio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted sio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted +class MyNamespace(socketio.Namespace): + def on_event2(self, sid, data): # $ requestHandler routedParameter=data + ensure_not_tainted(self, sid) + ensure_tainted(data) + res = self.call("e1", sid=sid) + ensure_tainted(res) # $ tainted + self.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + self.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + +sio.register_namespace(MyNamespace("/ns")) + asio = socketio.AsyncServer(async_mode='asgi') @asio.event -async def event2(sid, data): # $ requestHandler routedParameter=sid routedParameter=data +async def event3(sid, data): # $ requestHandler routedParameter=sid routedParameter=data ensure_not_tainted(sid) ensure_tainted(data) # $ tainted - res = await asio.call("e2", sid=sid) + res = await asio.call("e1", sid=sid) ensure_tainted(res) # $ tainted - await asio.emit("e3", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + await asio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted await asio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted if __name__ == "__main__": diff --git a/python/ql/test/library-tests/frameworks/socketio/test.py b/python/ql/test/library-tests/frameworks/socketio/test.py index c511316d51a8..f603edd3111f 100644 --- a/python/ql/test/library-tests/frameworks/socketio/test.py +++ b/python/ql/test/library-tests/frameworks/socketio/test.py @@ -3,23 +3,23 @@ sio = socketio.Server() @sio.on("connect") -def connect(sid, environ, auth): # $ requestHandler routedParameter=sid routedParameter=environ routedParameter=auth +def connect(sid, environ, auth): # $ requestHandler routedParameter=environ routedParameter=auth print("connect", sid, environ, auth) @sio.on("event1") -def handle(sid, data): # $ requestHandler routedParameter=sid routedParameter=data +def handle(sid, data): # $ requestHandler routedParameter=data print("e1", sid, data) @sio.event -def event2(sid, data): # $ requestHandler routedParameter=sid routedParameter=data +def event2(sid, data): # $ requestHandler routedParameter=data print("e2", sid, data) -def event3(sid, data): # $ requestHandler routedParameter=sid routedParameter=data +def event3(sid, data): # $ requestHandler routedParameter=data print("e3", sid, data) sio.on("event3", handler=event3) -sio.on("event4", lambda sid,data: print("e4", sid, data)) # $ requestHandler routedParameter=sid routedParameter=data +sio.on("event4", lambda sid,data: print("e4", sid, data)) # $ requestHandler routedParameter=data From eb7fe715571c21191d36a3cfb1aae42cc2e47d9b Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 26 Nov 2025 10:51:16 +0000 Subject: [PATCH 5/8] Fix namespace instances and update tests --- .../lib/semmle/python/frameworks/Socketio.qll | 6 +++- .../frameworks/socketio/taint_test.py | 29 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll index ca873d8ecf22..3df1901a1dd6 100644 --- a/python/ql/lib/semmle/python/frameworks/Socketio.qll +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -88,7 +88,11 @@ module SocketIO { } /** Gets a reference to an instance of a subclass of `socketio.Namespace` or `socketio.AsyncNamespace`. */ - API::Node instance() { result = subclassRef().getAnInstance() } + API::Node instance() { + result = subclassRef().getAnInstance() + or + result = subclassRef().getAMember().getSelfParameter() + } /** A socketio Namespace class. */ class NamespaceClass extends Class { diff --git a/python/ql/test/library-tests/frameworks/socketio/taint_test.py b/python/ql/test/library-tests/frameworks/socketio/taint_test.py index 0073e36ff60a..343c093feeb6 100644 --- a/python/ql/test/library-tests/frameworks/socketio/taint_test.py +++ b/python/ql/test/library-tests/frameworks/socketio/taint_test.py @@ -22,34 +22,45 @@ def event1(sid, data): # $ requestHandler routedParameter=data ensure_tainted(data) # $ tainted res = sio.call("e1", sid=sid) ensure_tainted(res) # $ tainted - sio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted - sio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + sio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x + sio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x class MyNamespace(socketio.Namespace): def on_event2(self, sid, data): # $ requestHandler routedParameter=data ensure_not_tainted(self, sid) - ensure_tainted(data) + ensure_tainted(data) # $ tainted res = self.call("e1", sid=sid) ensure_tainted(res) # $ tainted - self.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted - self.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + self.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x + self.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x sio.register_namespace(MyNamespace("/ns")) asio = socketio.AsyncServer(async_mode='asgi') @asio.event -async def event3(sid, data): # $ requestHandler routedParameter=sid routedParameter=data +async def event3(sid, data): # $ requestHandler routedParameter=data ensure_not_tainted(sid) ensure_tainted(data) # $ tainted res = await asio.call("e1", sid=sid) ensure_tainted(res) # $ tainted - await asio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted - await asio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted + await asio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x + await asio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x + +class MyAsyncNamespace(socketio.AsyncNamespace): + async def on_event4(self, sid, data): # $ requestHandler routedParameter=data + ensure_not_tainted(self, sid) + ensure_tainted(data) # $ tainted + res = await self.call("e1", sid=sid) + ensure_tainted(res) # $ tainted + await self.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x + await self.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x + +asio.register_namespace(MyAsyncNamespace("/ns")) if __name__ == "__main__": - if "--async" in sys.argv: + if "--async" in sys.argv: # $ threatModelSource[commandargs]=sys.argv import uvicorn app = socketio.ASGIApp(asio) uvicorn.run(app, host='127.0.0.1', port=8000) From 6207137ef0aaff035cdd419423e8abe1a8f38e99 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 26 Nov 2025 11:21:05 +0000 Subject: [PATCH 6/8] Add changenote --- python/ql/lib/change-notes/2025-11-26-socketio.md | 4 ++++ python/ql/lib/semmle/python/frameworks/Socketio.qll | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 python/ql/lib/change-notes/2025-11-26-socketio.md diff --git a/python/ql/lib/change-notes/2025-11-26-socketio.md b/python/ql/lib/change-notes/2025-11-26-socketio.md new file mode 100644 index 000000000000..e58bec0bbc12 --- /dev/null +++ b/python/ql/lib/change-notes/2025-11-26-socketio.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Remote flow sources for the `python-socketio` package have been modeled. \ No newline at end of file diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll index 3df1901a1dd6..f1c73400183b 100644 --- a/python/ql/lib/semmle/python/frameworks/Socketio.qll +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -1,10 +1,9 @@ /** - * Provides definitions and modelling for the `python-socketio` PyPI package. + * Provides definitions and modeling for the `python-socketio` PyPI package. * See https://python-socketio.readthedocs.io/en/stable/. */ private import python -private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.Concepts @@ -79,7 +78,7 @@ module SocketIO { } } - /** Provides modelling for socketio server Namespace/AsyncNamespace classes. */ + /** Provides modeling for socketio server Namespace/AsyncNamespace classes. */ module Namespace { /** Gets a reference to the `socketio.Namespace` or `socketio.AsyncNamespace` classes or any subclass. */ API::Node subclassRef() { From 8d313ff85b23805b7d2e41e5c1745eebef1f5a68 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 26 Nov 2025 11:23:04 +0000 Subject: [PATCH 7/8] qldoc fixes --- python/ql/lib/semmle/python/frameworks/Socketio.qll | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/Socketio.qll b/python/ql/lib/semmle/python/frameworks/Socketio.qll index f1c73400183b..4006dcfbe7d8 100644 --- a/python/ql/lib/semmle/python/frameworks/Socketio.qll +++ b/python/ql/lib/semmle/python/frameworks/Socketio.qll @@ -17,12 +17,12 @@ private import semmle.python.frameworks.internal.PoorMansFunctionResolution module SocketIO { /** Provides models for socketio `Server` and `AsyncServer` classes. */ module Server { - /** An instance of a socketio `Server` or `AsyncServer`. */ + /** Gets an instance of a socketio `Server` or `AsyncServer`. */ API::Node server() { result = API::moduleImport("socketio").getMember(["Server", "AsyncServer"]).getAnInstance() } - /** A decorator that indicates a socketio event handler. */ + /** Gets a decorator that indicates a socketio event handler. */ private API::Node serverEventAnnotation() { result = server().getMember("event") or From 16018e91a27a5a4aa8928ea5909cae2477dc5dc5 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 26 Nov 2025 15:47:56 +0000 Subject: [PATCH 8/8] Minor test fix --- python/ql/test/library-tests/frameworks/socketio/taint_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/ql/test/library-tests/frameworks/socketio/taint_test.py b/python/ql/test/library-tests/frameworks/socketio/taint_test.py index 343c093feeb6..07d109aa9a2f 100644 --- a/python/ql/test/library-tests/frameworks/socketio/taint_test.py +++ b/python/ql/test/library-tests/frameworks/socketio/taint_test.py @@ -1,6 +1,5 @@ import sys import socketio -import sys def ensure_tainted(*args): print("tainted", args)