Skip to content

Commit d70c596

Browse files
Merge pull request #20914 from joefarebrother/python-socketio
Python: Add models for socketio
2 parents 8b89e15 + 16018e9 commit d70c596

File tree

9 files changed

+229
-0
lines changed

9 files changed

+229
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Remote flow sources for the `python-socketio` package have been modeled.

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ private import semmle.python.frameworks.Sanic
7878
private import semmle.python.frameworks.ServerLess
7979
private import semmle.python.frameworks.Setuptools
8080
private import semmle.python.frameworks.Simplejson
81+
private import semmle.python.frameworks.Socketio
8182
private import semmle.python.frameworks.SqlAlchemy
8283
private import semmle.python.frameworks.Starlette
8384
private import semmle.python.frameworks.Stdlib
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Provides definitions and modeling for the `python-socketio` PyPI package.
3+
* See https://python-socketio.readthedocs.io/en/stable/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.TaintTracking
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.Concepts
10+
private import semmle.python.ApiGraphs
11+
private import semmle.python.frameworks.internal.PoorMansFunctionResolution
12+
13+
/**
14+
* Provides models for the `python-socketio` PyPI package.
15+
* See https://python-socketio.readthedocs.io/en/stable/.
16+
*/
17+
module SocketIO {
18+
/** Provides models for socketio `Server` and `AsyncServer` classes. */
19+
module Server {
20+
/** Gets an instance of a socketio `Server` or `AsyncServer`. */
21+
API::Node server() {
22+
result = API::moduleImport("socketio").getMember(["Server", "AsyncServer"]).getAnInstance()
23+
}
24+
25+
/** Gets a decorator that indicates a socketio event handler. */
26+
private API::Node serverEventAnnotation() {
27+
result = server().getMember("event")
28+
or
29+
result = server().getMember("on").getReturn()
30+
}
31+
32+
private class EventHandler extends Http::Server::RequestHandler::Range {
33+
EventHandler() {
34+
serverEventAnnotation().getAValueReachableFromSource().asExpr() = this.getADecorator()
35+
or
36+
exists(DataFlow::CallCfgNode c, DataFlow::Node arg |
37+
c = server().getMember("on").getACall()
38+
|
39+
(
40+
arg = c.getArg(1)
41+
or
42+
arg = c.getArgByName("handler")
43+
) and
44+
poorMansFunctionTracker(this) = arg
45+
)
46+
}
47+
48+
override Parameter getARoutedParameter() {
49+
result = this.getAnArg() and
50+
not result = this.getArg(0) // First parameter is `sid`, which is not a remote flow source as it cannot be controlled by the client.
51+
}
52+
53+
override string getFramework() { result = "socketio" }
54+
}
55+
56+
private class CallbackArgument extends DataFlow::Node {
57+
CallbackArgument() {
58+
exists(DataFlow::CallCfgNode c |
59+
c = [server(), Namespace::instance()].getMember(["emit", "send"]).getACall()
60+
|
61+
this = c.getArgByName("callback")
62+
)
63+
}
64+
}
65+
66+
private class CallbackHandler extends Http::Server::RequestHandler::Range {
67+
CallbackHandler() { any(CallbackArgument ca) = poorMansFunctionTracker(this) }
68+
69+
override Parameter getARoutedParameter() { result = this.getAnArg() }
70+
71+
override string getFramework() { result = "socketio" }
72+
}
73+
74+
private class SocketIOCall extends RemoteFlowSource::Range {
75+
SocketIOCall() { this = [server(), Namespace::instance()].getMember("call").getACall() }
76+
77+
override string getSourceType() { result = "socketio call" }
78+
}
79+
}
80+
81+
/** Provides modeling for socketio server Namespace/AsyncNamespace classes. */
82+
module Namespace {
83+
/** Gets a reference to the `socketio.Namespace` or `socketio.AsyncNamespace` classes or any subclass. */
84+
API::Node subclassRef() {
85+
result =
86+
API::moduleImport("socketio").getMember(["Namespace", "AsyncNamespace"]).getASubclass*()
87+
}
88+
89+
/** Gets a reference to an instance of a subclass of `socketio.Namespace` or `socketio.AsyncNamespace`. */
90+
API::Node instance() {
91+
result = subclassRef().getAnInstance()
92+
or
93+
result = subclassRef().getAMember().getSelfParameter()
94+
}
95+
96+
/** A socketio Namespace class. */
97+
class NamespaceClass extends Class {
98+
NamespaceClass() { this.getABase() = subclassRef().asSource().asExpr() }
99+
100+
/** Gets a handler for socketio events. */
101+
Function getAnEventHandler() {
102+
result = this.getAMethod() and
103+
result.getName().matches("on_%")
104+
}
105+
}
106+
107+
private class NamespaceEventHandler extends Http::Server::RequestHandler::Range {
108+
NamespaceEventHandler() { this = any(NamespaceClass nc).getAnEventHandler() }
109+
110+
override Parameter getARoutedParameter() {
111+
result = this.getAnArg() and
112+
not result = this.getArg(0) and
113+
not result = this.getArg(1) // First 2 parameters are `self` and `sid`.
114+
}
115+
116+
override string getFramework() { result = "socketio" }
117+
}
118+
}
119+
}

python/ql/test/library-tests/frameworks/socketio/ConceptsTest.expected

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.ConceptsTest
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
argumentToEnsureNotTaintedNotMarkedAsSpurious
2+
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
3+
testFailures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import experimental.meta.InlineTaintTest
2+
import MakeInlineTaintTest<TestTaintTrackingConfig>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import sys
2+
import socketio
3+
4+
def ensure_tainted(*args):
5+
print("tainted", args)
6+
7+
def ensure_not_tainted(*args):
8+
print("not tainted", args)
9+
10+
sio = socketio.Server()
11+
12+
@sio.event
13+
def connect(sid, environ, auth): # $ requestHandler routedParameter=environ routedParameter=auth
14+
ensure_not_tainted(sid)
15+
ensure_tainted(environ, # $ tainted
16+
auth) # $ tainted
17+
18+
@sio.event
19+
def event1(sid, data): # $ requestHandler routedParameter=data
20+
ensure_not_tainted(sid)
21+
ensure_tainted(data) # $ tainted
22+
res = sio.call("e1", sid=sid)
23+
ensure_tainted(res) # $ tainted
24+
sio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
25+
sio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
26+
27+
class MyNamespace(socketio.Namespace):
28+
def on_event2(self, sid, data): # $ requestHandler routedParameter=data
29+
ensure_not_tainted(self, sid)
30+
ensure_tainted(data) # $ tainted
31+
res = self.call("e1", sid=sid)
32+
ensure_tainted(res) # $ tainted
33+
self.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
34+
self.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
35+
36+
sio.register_namespace(MyNamespace("/ns"))
37+
38+
asio = socketio.AsyncServer(async_mode='asgi')
39+
40+
@asio.event
41+
async def event3(sid, data): # $ requestHandler routedParameter=data
42+
ensure_not_tainted(sid)
43+
ensure_tainted(data) # $ tainted
44+
res = await asio.call("e1", sid=sid)
45+
ensure_tainted(res) # $ tainted
46+
await asio.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
47+
await asio.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
48+
49+
class MyAsyncNamespace(socketio.AsyncNamespace):
50+
async def on_event4(self, sid, data): # $ requestHandler routedParameter=data
51+
ensure_not_tainted(self, sid)
52+
ensure_tainted(data) # $ tainted
53+
res = await self.call("e1", sid=sid)
54+
ensure_tainted(res) # $ tainted
55+
await self.emit("e2", "hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
56+
await self.send("hi", to=sid, callback=lambda x: ensure_tainted(x)) # $ tainted $ requestHandler routedParameter=x
57+
58+
asio.register_namespace(MyAsyncNamespace("/ns"))
59+
60+
if __name__ == "__main__":
61+
62+
if "--async" in sys.argv: # $ threatModelSource[commandargs]=sys.argv
63+
import uvicorn
64+
app = socketio.ASGIApp(asio)
65+
uvicorn.run(app, host='127.0.0.1', port=8000)
66+
else:
67+
import eventlet
68+
app = socketio.WSGIApp(sio)
69+
eventlet.wsgi.server(eventlet.listen(('', 8000)), app)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import socketio
2+
3+
sio = socketio.Server()
4+
5+
@sio.on("connect")
6+
def connect(sid, environ, auth): # $ requestHandler routedParameter=environ routedParameter=auth
7+
print("connect", sid, environ, auth)
8+
9+
@sio.on("event1")
10+
def handle(sid, data): # $ requestHandler routedParameter=data
11+
print("e1", sid, data)
12+
13+
@sio.event
14+
def event2(sid, data): # $ requestHandler routedParameter=data
15+
print("e2", sid, data)
16+
17+
def event3(sid, data): # $ requestHandler routedParameter=data
18+
print("e3", sid, data)
19+
20+
sio.on("event3", handler=event3)
21+
22+
sio.on("event4", lambda sid,data: print("e4", sid, data)) # $ requestHandler routedParameter=data
23+
24+
25+
26+
if __name__ == "__main__":
27+
app = socketio.WSGIApp(sio)
28+
import eventlet
29+
eventlet.wsgi.server(eventlet.listen(('', 8000)), app)

0 commit comments

Comments
 (0)