Skip to content

Commit d55f18f

Browse files
committed
Python: Add modeling of Flask-SQLAlchemy
1 parent f174489 commit d55f18f

File tree

6 files changed

+73
-11
lines changed

6 files changed

+73
-11
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ Python built-in support
176176
mysqlclient, Database
177177
psycopg2, Database
178178
sqlite3, Database
179+
Flask-SQLAlchemy, Database ORM
179180
peewee, Database ORM
180181
SQLAlchemy, Database ORM
181182
cryptography, Cryptography library
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added modeling of SQL execution in the `Flask-SQLAlchemy` PyPI package, resulting in additional sinks for the SQL Injection query (`py/sql-injection`).

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ private import semmle.python.frameworks.Dill
1313
private import semmle.python.frameworks.Django
1414
private import semmle.python.frameworks.Fabric
1515
private import semmle.python.frameworks.Flask
16+
private import semmle.python.frameworks.FlaskSqlAlchemy
1617
private import semmle.python.frameworks.Idna
1718
private import semmle.python.frameworks.Invoke
1819
private import semmle.python.frameworks.Jmespath
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `Flask-SQLAlchemy` PyPI package
3+
* (imported by `flask_sqlalchemy`).
4+
* See
5+
* - https://pypi.org/project/Flask-SQLAlchemy/
6+
* - https://flask-sqlalchemy.palletsprojects.com/en/2.x/
7+
*/
8+
9+
private import python
10+
private import semmle.python.dataflow.new.DataFlow
11+
private import semmle.python.dataflow.new.TaintTracking
12+
private import semmle.python.ApiGraphs
13+
private import semmle.python.Concepts
14+
private import semmle.python.frameworks.SqlAlchemy
15+
16+
/**
17+
* INTERNAL: Do not use.
18+
*
19+
* Provides models for the `Flask-SQLAlchemy` PyPI package (imported by `flask_sqlalchemy`).
20+
* See
21+
* - https://pypi.org/project/Flask-SQLAlchemy/
22+
* - https://flask-sqlalchemy.palletsprojects.com/en/2.x/
23+
*/
24+
private module FlaskSqlAlchemy {
25+
/** Gets an instance of `flask_sqlalchemy.SQLAlchemy` */
26+
private API::Node dbInstance() {
27+
result = API::moduleImport("flask_sqlalchemy").getMember("SQLAlchemy").getReturn()
28+
}
29+
30+
/** A call to the `text` method on a DB. */
31+
private class DbTextCall extends SqlAlchemy::TextClause::TextClauseConstruction {
32+
DbTextCall() { this = dbInstance().getMember("text").getACall() }
33+
}
34+
35+
/** Access on a DB resulting in an Engine */
36+
private class DbEngine extends SqlAlchemy::Engine::InstanceSource {
37+
DbEngine() {
38+
this = dbInstance().getMember("engine").getAUse()
39+
or
40+
this = dbInstance().getMember("get_engine").getACall()
41+
}
42+
}
43+
44+
/** Access on a DB resulting in a Session */
45+
private class DbSession extends SqlAlchemy::Session::InstanceSource {
46+
DbSession() {
47+
this = dbInstance().getMember("session").getAUse()
48+
or
49+
this = dbInstance().getMember("create_session").getReturn().getACall()
50+
or
51+
this = dbInstance().getMember("create_session").getReturn().getMember("begin").getACall()
52+
or
53+
this = dbInstance().getMember("create_scoped_session").getACall()
54+
}
55+
}
56+
}

python/ql/lib/semmle/python/frameworks/SqlAlchemy.qll

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,13 @@ module SqlAlchemy {
314314
* A construction of a `sqlalchemy.sql.expression.TextClause`, which represents a
315315
* textual SQL string directly.
316316
*/
317-
class TextClauseConstruction extends DataFlow::CallCfgNode {
318-
TextClauseConstruction() {
317+
abstract class TextClauseConstruction extends DataFlow::CallCfgNode {
318+
/** Gets the argument that specifies the SQL text. */
319+
DataFlow::Node getTextArg() { result in [this.getArg(0), this.getArgByName("text")] }
320+
}
321+
322+
class DefaultTextClauseConstruction extends TextClauseConstruction {
323+
DefaultTextClauseConstruction() {
319324
this = API::moduleImport("sqlalchemy").getMember("text").getACall()
320325
or
321326
this = API::moduleImport("sqlalchemy").getMember("sql").getMember("text").getACall()
@@ -334,9 +339,6 @@ module SqlAlchemy {
334339
.getMember("TextClause")
335340
.getACall()
336341
}
337-
338-
/** Gets the argument that specifies the SQL text. */
339-
DataFlow::Node getTextArg() { result in [this.getArg(0), this.getArgByName("text")] }
340342
}
341343
}
342344
}

python/ql/test/library-tests/frameworks/flask_sqlalchemy/SqlExecution.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,27 @@
1919
raw_sql = "SELECT 'Foo'"
2020

2121
conn = db.engine.connect()
22-
result = conn.execute(raw_sql) # $ MISSING: getSql=raw_sql
22+
result = conn.execute(raw_sql) # $ getSql=raw_sql
2323
assert result.fetchall() == [("Foo",)]
2424

2525
conn = db.get_engine().connect()
26-
result = conn.execute(raw_sql) # $ MISSING: getSql=raw_sql
26+
result = conn.execute(raw_sql) # $ getSql=raw_sql
2727
assert result.fetchall() == [("Foo",)]
2828

29-
result = db.session.execute(raw_sql) # $ MISSING: getSql=raw_sql
29+
result = db.session.execute(raw_sql) # $ getSql=raw_sql
3030
assert result.fetchall() == [("Foo",)]
3131

3232
Session = db.create_session(options={})
3333
session = Session()
34-
result = session.execute(raw_sql) # $ MISSING: getSql=raw_sql
34+
result = session.execute(raw_sql) # $ getSql=raw_sql
3535
assert result.fetchall() == [("Foo",)]
3636

3737
Session = db.create_session(options={})
3838
with Session.begin() as session:
39-
result = session.execute(raw_sql) # $ MISSING: getSql=raw_sql
39+
result = session.execute(raw_sql) # $ getSql=raw_sql
4040
assert result.fetchall() == [("Foo",)]
4141

42-
result = db.create_scoped_session().execute(raw_sql) # $ MISSING: getSql=raw_sql
42+
result = db.create_scoped_session().execute(raw_sql) # $ getSql=raw_sql
4343
assert result.fetchall() == [("Foo",)]
4444

4545

0 commit comments

Comments
 (0)