Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions python/ql/lib/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Socketio.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* 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.dataflow.new.RemoteFlowSources
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 {
/** 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()
}

/** A decorator that indicates a socketio event handler. */
private 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()
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" }
}

private class CallbackArgument extends DataFlow::Node {
CallbackArgument() {
exists(DataFlow::CallCfgNode c |
c = [server(), Namespace::instance()].getMember(["emit", "send"]).getACall()
|
this = c.getArgByName("callback")
)
}
}

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" }
}

private class SocketIOCall extends RemoteFlowSource::Range {
SocketIOCall() { this = [server(), Namespace::instance()].getMember("call").getACall() }

override string getSourceType() { result = "socketio call" }
}
}

/** 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*()
}

/** Gets a reference to an instance of a subclass of `socketio.Namespace` or `socketio.AsyncNamespace`. */
API::Node instance() { result = subclassRef().getAnInstance() }

/** A socketio Namespace class. */
class NamespaceClass extends Class {
NamespaceClass() { this.getABase() = subclassRef().asSource().asExpr() }

/** 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 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" }
}
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
argumentToEnsureNotTaintedNotMarkedAsSpurious
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
testFailures
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import experimental.meta.InlineTaintTest
import MakeInlineTaintTest<TestTaintTrackingConfig>
59 changes: 59 additions & 0 deletions python/ql/test/library-tests/frameworks/socketio/taint_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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=environ routedParameter=auth
ensure_not_tainted(sid)
ensure_tainted(environ, # $ tainted
auth) # $ tainted

@sio.event
def event1(sid, data): # $ requestHandler 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

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 event3(sid, data): # $ requestHandler routedParameter=sid 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

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)
29 changes: 29 additions & 0 deletions python/ql/test/library-tests/frameworks/socketio/test.py
Original file line number Diff line number Diff line change
@@ -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)