Skip to content

Commit fe143c7

Browse files
committed
Python: Rewrite most of SQLAlchemy modeling
1 parent b39bb24 commit fe143c7

File tree

2 files changed

+267
-105
lines changed

2 files changed

+267
-105
lines changed
Lines changed: 224 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
2-
* Provides classes modeling security-relevant aspects of the 'SqlAlchemy' package.
3-
* See https://pypi.org/project/SQLAlchemy/.
2+
* Provides classes modeling security-relevant aspects of the `SQLAlchemy` PyPI package.
3+
* See
4+
* - https://pypi.org/project/SQLAlchemy/
5+
* - https://docs.sqlalchemy.org/en/14/index.html
46
*/
57

68
private import python
@@ -10,93 +12,209 @@ private import semmle.python.ApiGraphs
1012
private import semmle.python.Concepts
1113
private import experimental.semmle.python.Concepts
1214

15+
/**
16+
* Provides models for the `SQLAlchemy` PyPI package.
17+
* See
18+
* - https://pypi.org/project/SQLAlchemy/
19+
* - https://docs.sqlalchemy.org/en/14/index.html
20+
*/
1321
private module SqlAlchemy {
1422
/**
15-
* Returns an instantization of a SqlAlchemy Session object.
16-
* See https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session and
17-
* https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.sessionmaker
23+
* Provides models for the `sqlalchemy.engine.Engine` and `sqlalchemy.future.Engine` classes.
24+
*
25+
* These are so similar that we model both in the same way.
26+
*
27+
* See
28+
* - https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Engine
29+
* - https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Engine
1830
*/
19-
private API::Node getSqlAlchemySessionInstance() {
20-
result = API::moduleImport("sqlalchemy.orm").getMember("Session").getReturn() or
21-
result = API::moduleImport("sqlalchemy.orm").getMember("sessionmaker").getReturn().getReturn()
22-
}
31+
module Engine {
32+
/** Gets a reference to a SQLAlchemy Engine class. */
33+
private API::Node classRef() {
34+
result = API::moduleImport("sqlalchemy").getMember("engine").getMember("Engine")
35+
or
36+
result = API::moduleImport("sqlalchemy").getMember("future").getMember("Engine")
37+
}
2338

24-
/**
25-
* Returns an instantization of a SqlAlchemy Engine object.
26-
* See https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine
27-
*/
28-
private API::Node getSqlAlchemyEngineInstance() {
29-
result = API::moduleImport("sqlalchemy").getMember("create_engine").getReturn()
30-
}
39+
/**
40+
* A source of instances of a SQLAlchemy Engine, extend this class to model new instances.
41+
*
42+
* This can include instantiations of the class, return values from function
43+
* calls, or a special parameter that will be set when functions are called by an external
44+
* library.
45+
*
46+
* Use the predicate `Engine::instance()` to get references to instances of a SQLAlchemy Engine.
47+
*/
48+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
3149

32-
/**
33-
* Returns an instantization of a SqlAlchemy Query object.
34-
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
35-
*/
36-
private API::Node getSqlAlchemyQueryInstance() {
37-
result = getSqlAlchemySessionInstance().getMember("query").getReturn()
50+
private class EngineConstruction extends InstanceSource, DataFlow::CallCfgNode {
51+
EngineConstruction() {
52+
this = classRef().getACall()
53+
or
54+
this = API::moduleImport("sqlalchemy").getMember("create_engine").getACall()
55+
or
56+
this =
57+
API::moduleImport("sqlalchemy").getMember("future").getMember("create_engine").getACall()
58+
or
59+
this.(DataFlow::MethodCallNode).calls(instance(), "execution_options")
60+
}
61+
}
62+
63+
/** Gets a reference to an instance of a SQLAlchemy Engine. */
64+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
65+
t.start() and
66+
result instanceof InstanceSource
67+
or
68+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
69+
}
70+
71+
/** Gets a reference to an instance of a SQLAlchemy Engine. */
72+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
3873
}
3974

4075
/**
41-
* A call to `execute` meant to execute an SQL expression
42-
* See the following links:
43-
* - https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=execute#sqlalchemy.engine.Connection.execute
44-
* - https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=execute#sqlalchemy.engine.Engine.execute
45-
* - https://docs.sqlalchemy.org/en/14/orm/session_api.html?highlight=execute#sqlalchemy.orm.Session.execute
76+
* Provides models for the `sqlalchemy.engine.base.Connection` and `sqlalchemy.future.Connection` classes.
77+
*
78+
* These are so similar that we model both in the same way.
79+
*
80+
* See
81+
* - https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Connection
82+
* - https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Connection
4683
*/
47-
private class SqlAlchemyExecuteCall extends DataFlow::CallCfgNode, SqlExecution::Range {
48-
SqlAlchemyExecuteCall() {
49-
// new way
50-
this = getSqlAlchemySessionInstance().getMember("execute").getACall() or
51-
this =
52-
getSqlAlchemyEngineInstance()
53-
.getMember(["connect", "begin"])
54-
.getReturn()
55-
.getMember("execute")
56-
.getACall()
84+
module Connection {
85+
/** Gets a reference to a SQLAlchemy Connection class. */
86+
private API::Node classRef() {
87+
result =
88+
API::moduleImport("sqlalchemy")
89+
.getMember("engine")
90+
.getMember("base")
91+
.getMember("Connection")
92+
or
93+
result = API::moduleImport("sqlalchemy").getMember("future").getMember("Connection")
5794
}
5895

59-
override DataFlow::Node getSql() { result = this.getArg(0) }
96+
/**
97+
* A source of instances of a SQLAlchemy Connection, extend this class to model new instances.
98+
*
99+
* This can include instantiations of the class, return values from function
100+
* calls, or a special parameter that will be set when functions are called by an external
101+
* library.
102+
*
103+
* Use the predicate `Connection::instance()` to get references to instances of a SQLAlchemy Connection.
104+
*/
105+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
106+
107+
private class ConnectionConstruction extends InstanceSource, DataFlow::CallCfgNode {
108+
ConnectionConstruction() {
109+
this = classRef().getACall()
110+
or
111+
this.(DataFlow::MethodCallNode).calls(Engine::instance(), ["begin", "connect"])
112+
or
113+
this.(DataFlow::MethodCallNode).calls(instance(), "connect")
114+
}
115+
}
116+
117+
/** Gets a reference to an instance of a SQLAlchemy Connection. */
118+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
119+
t.start() and
120+
result instanceof InstanceSource
121+
or
122+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
123+
}
124+
125+
/** Gets a reference to an instance of a SQLAlchemy Connection. */
126+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
60127
}
61128

62129
/**
63-
* A call to `scalar` meant to execute an SQL expression
64-
* See https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session.scalar and
65-
* https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=scalar#sqlalchemy.engine.Engine.scalar
130+
* Provides models for the `sqlalchemy.orm.Session` class
131+
*
132+
* See
133+
* - https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session
134+
* - https://docs.sqlalchemy.org/en/14/orm/session_basics.html
66135
*/
67-
private class SqlAlchemyScalarCall extends DataFlow::CallCfgNode, SqlExecution::Range {
68-
SqlAlchemyScalarCall() {
69-
this =
70-
[getSqlAlchemySessionInstance(), getSqlAlchemyEngineInstance()]
71-
.getMember("scalar")
72-
.getACall()
136+
module Session {
137+
/** Gets a reference to the `sqlalchemy.orm.Session` class. */
138+
private API::Node classRef() {
139+
result = API::moduleImport("sqlalchemy").getMember("orm").getMember("Session")
73140
}
74141

75-
override DataFlow::Node getSql() { result = this.getArg(0) }
142+
/**
143+
* A source of instances of `sqlalchemy.orm.Session`, extend this class to model new instances.
144+
*
145+
* This can include instantiations of the class, return values from function
146+
* calls, or a special parameter that will be set when functions are called by an external
147+
* library.
148+
*
149+
* Use the predicate `Session::instance()` to get references to instances of `sqlalchemy.orm.Session`.
150+
*/
151+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
152+
153+
private class SessionConstruction extends InstanceSource, DataFlow::CallCfgNode {
154+
SessionConstruction() {
155+
this = classRef().getACall()
156+
or
157+
this =
158+
API::moduleImport("sqlalchemy")
159+
.getMember("orm")
160+
.getMember("sessionmaker")
161+
.getReturn()
162+
.getACall()
163+
}
164+
}
165+
166+
/** Gets a reference to an instance of `sqlalchemy.orm.Session`. */
167+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
168+
t.start() and
169+
result instanceof InstanceSource
170+
or
171+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
172+
}
173+
174+
/** Gets a reference to an instance of `sqlalchemy.orm.Session`. */
175+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
76176
}
77177

78178
/**
79-
* A call on a Query object
80-
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
179+
* A call to `execute` on a SQLAlchemy Engine, Connection, or Session.
180+
* See
181+
* - https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Engine.execute
182+
* - https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Connection.execute
183+
* - https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Connection.execute
184+
* - https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session.execute
81185
*/
82-
private class SqlAlchemyQueryCall extends DataFlow::CallCfgNode, SqlExecution::Range {
83-
SqlAlchemyQueryCall() {
84-
this =
85-
getSqlAlchemyQueryInstance()
86-
.getMember(any(SqlAlchemyVulnerableMethodNames methodName))
87-
.getACall()
186+
private class SqlAlchemyExecuteCall extends DataFlow::MethodCallNode, SqlExecution::Range {
187+
SqlAlchemyExecuteCall() {
188+
this.calls(Engine::instance(), "execute")
189+
or
190+
this.calls(Connection::instance(), "execute")
191+
or
192+
this.calls(Session::instance(), "execute")
88193
}
89194

90-
override DataFlow::Node getSql() { result = this.getArg(0) }
195+
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("statement")] }
91196
}
92197

93198
/**
94-
* This class represents a list of methods vulnerable to sql injection.
95-
*
96-
* See https://github.com/jty-team/codeql/pull/2#issue-611592361
199+
* A call to `scalar` on a SQLAlchemy Engine, Connection, or Session.
200+
* See
201+
* - https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Engine.scalar
202+
* - https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Connection.scalar
203+
* - https://docs.sqlalchemy.org/en/14/core/future.html#sqlalchemy.future.Connection.scalar
204+
* - https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session.scalar
97205
*/
98-
private class SqlAlchemyVulnerableMethodNames extends string {
99-
SqlAlchemyVulnerableMethodNames() { this in ["filter", "filter_by", "group_by", "order_by"] }
206+
private class SqlAlchemyScalarCall extends DataFlow::MethodCallNode, SqlExecution::Range {
207+
SqlAlchemyScalarCall() {
208+
this.calls(Engine::instance(), "scalar")
209+
or
210+
this.calls(Connection::instance(), "scalar")
211+
or
212+
this.calls(Session::instance(), "scalar")
213+
}
214+
215+
override DataFlow::Node getSql() {
216+
result in [this.getArg(0), this.getArgByName("statement"), this.getArgByName("object_")]
217+
}
100218
}
101219

102220
/**
@@ -146,3 +264,47 @@ private module SqlAlchemy {
146264
override DataFlow::Node getAnInput() { result = this.getArg(0) }
147265
}
148266
}
267+
268+
private module OldModeling {
269+
/**
270+
* Returns an instantization of a SqlAlchemy Session object.
271+
* See https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session and
272+
* https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.sessionmaker
273+
*/
274+
private API::Node getSqlAlchemySessionInstance() {
275+
result = API::moduleImport("sqlalchemy.orm").getMember("Session").getReturn() or
276+
result = API::moduleImport("sqlalchemy.orm").getMember("sessionmaker").getReturn().getReturn()
277+
}
278+
279+
/**
280+
* Returns an instantization of a SqlAlchemy Query object.
281+
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
282+
*/
283+
private API::Node getSqlAlchemyQueryInstance() {
284+
result = getSqlAlchemySessionInstance().getMember("query").getReturn()
285+
}
286+
287+
/**
288+
* A call on a Query object
289+
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
290+
*/
291+
private class SqlAlchemyQueryCall extends DataFlow::CallCfgNode, SqlExecution::Range {
292+
SqlAlchemyQueryCall() {
293+
this =
294+
getSqlAlchemyQueryInstance()
295+
.getMember(any(SqlAlchemyVulnerableMethodNames methodName))
296+
.getACall()
297+
}
298+
299+
override DataFlow::Node getSql() { result = this.getArg(0) }
300+
}
301+
302+
/**
303+
* This class represents a list of methods vulnerable to sql injection.
304+
*
305+
* See https://github.com/jty-team/codeql/pull/2#issue-611592361
306+
*/
307+
private class SqlAlchemyVulnerableMethodNames extends string {
308+
SqlAlchemyVulnerableMethodNames() { this in ["filter", "filter_by", "group_by", "order_by"] }
309+
}
310+
}

0 commit comments

Comments
 (0)