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
68private import python
@@ -10,93 +12,209 @@ private import semmle.python.ApiGraphs
1012private import semmle.python.Concepts
1113private 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+ */
1321private 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