Skip to content

Commit 25e88ed

Browse files
authored
Merge pull request github#4588 from yoff/python-pep-249
Python: Model PEP 249
2 parents 2dfffdb + 1535ce1 commit 25e88ed

File tree

11 files changed

+296
-54
lines changed

11 files changed

+296
-54
lines changed

python/ql/src/experimental/semmle/python/Frameworks.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ private import experimental.semmle.python.frameworks.Django
77
private import experimental.semmle.python.frameworks.Fabric
88
private import experimental.semmle.python.frameworks.Flask
99
private import experimental.semmle.python.frameworks.Invoke
10+
private import experimental.semmle.python.frameworks.MySQLdb
11+
private import experimental.semmle.python.frameworks.MysqlConnectorPython
1012
private import experimental.semmle.python.frameworks.Stdlib
1113
private import experimental.semmle.python.frameworks.Yaml

python/ql/src/experimental/semmle/python/frameworks/Django.qll

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import experimental.dataflow.DataFlow
88
private import experimental.dataflow.RemoteFlowSources
99
private import experimental.dataflow.TaintTracking
1010
private import experimental.semmle.python.Concepts
11+
private import experimental.semmle.python.frameworks.PEP249
1112
private import semmle.python.regex
1213

1314
/**
@@ -76,6 +77,10 @@ private module Django {
7677
/** Gets a reference to the `django.db` module. */
7778
DataFlow::Node db() { result = django_attr("db") }
7879

80+
class DjangoDb extends PEP249Module {
81+
DjangoDb() { this = db() }
82+
}
83+
7984
/** Provides models for the `django.db` module. */
8085
module db {
8186
/** Gets a reference to the `django.db.connection` object. */
@@ -92,45 +97,10 @@ private module Django {
9297
/** Gets a reference to the `django.db.connection` object. */
9398
DataFlow::Node connection() { result = connection(DataFlow::TypeTracker::end()) }
9499

95-
/** Provides models for the `django.db.connection.cursor` method. */
96-
module cursor {
97-
/** Gets a reference to the `django.db.connection.cursor` metod. */
98-
private DataFlow::Node methodRef(DataFlow::TypeTracker t) {
99-
t.start() and
100-
result = DataFlow::importNode("django.db.connection.cursor")
101-
or
102-
t.startInAttr("cursor") and
103-
result = connection()
104-
or
105-
exists(DataFlow::TypeTracker t2 | result = methodRef(t2).track(t2, t))
106-
}
107-
108-
/** Gets a reference to the `django.db.connection.cursor` metod. */
109-
DataFlow::Node methodRef() { result = methodRef(DataFlow::TypeTracker::end()) }
110-
111-
/** Gets a reference to a result of calling `django.db.connection.cursor`. */
112-
private DataFlow::Node methodResult(DataFlow::TypeTracker t) {
113-
t.start() and
114-
result.asCfgNode().(CallNode).getFunction() = methodRef().asCfgNode()
115-
or
116-
exists(DataFlow::TypeTracker t2 | result = methodResult(t2).track(t2, t))
117-
}
118-
119-
/** Gets a reference to a result of calling `django.db.connection.cursor`. */
120-
DataFlow::Node methodResult() { result = methodResult(DataFlow::TypeTracker::end()) }
100+
class DjangoDbConnection extends Connection::InstanceSource {
101+
DjangoDbConnection() { this = connection() }
121102
}
122103

123-
/** Gets a reference to the `django.db.connection.cursor.execute` function. */
124-
private DataFlow::Node execute(DataFlow::TypeTracker t) {
125-
t.startInAttr("execute") and
126-
result = cursor::methodResult()
127-
or
128-
exists(DataFlow::TypeTracker t2 | result = execute(t2).track(t2, t))
129-
}
130-
131-
/** Gets a reference to the `django.db.connection.cursor.execute` function. */
132-
DataFlow::Node execute() { result = execute(DataFlow::TypeTracker::end()) }
133-
134104
// -------------------------------------------------------------------------
135105
// django.db.models
136106
// -------------------------------------------------------------------------
@@ -276,23 +246,6 @@ private module Django {
276246
}
277247
}
278248

279-
/**
280-
* A call to the `django.db.connection.cursor.execute` function.
281-
*
282-
* See
283-
* - https://docs.djangoproject.com/en/3.1/topics/db/sql/#executing-custom-sql-directly
284-
* - https://docs.djangoproject.com/en/3.1/topics/db/sql/#connections-and-cursors
285-
*/
286-
private class DbConnectionExecute extends SqlExecution::Range, DataFlow::CfgNode {
287-
override CallNode node;
288-
289-
DbConnectionExecute() { node.getFunction() = django::db::execute().asCfgNode() }
290-
291-
override DataFlow::Node getSql() {
292-
result.asCfgNode() in [node.getArg(0), node.getArgByName("sql")]
293-
}
294-
}
295-
296249
/**
297250
* A call to the `annotate` function on a model using a `RawSQL` argument.
298251
*
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `MySQLdb` PyPI package.
3+
* See
4+
* - https://mysqlclient.readthedocs.io/index.html
5+
* - https://pypi.org/project/MySQL-python/
6+
*/
7+
8+
private import python
9+
private import experimental.dataflow.DataFlow
10+
private import experimental.dataflow.RemoteFlowSources
11+
private import experimental.semmle.python.Concepts
12+
private import PEP249
13+
14+
/**
15+
* Provides models for the `MySQLdb` PyPI package.
16+
* See
17+
* - https://mysqlclient.readthedocs.io/index.html
18+
* - https://pypi.org/project/MySQL-python/
19+
*/
20+
module MySQLdb {
21+
// ---------------------------------------------------------------------------
22+
// MySQLdb
23+
// ---------------------------------------------------------------------------
24+
/** Gets a reference to the `MySQLdb` module. */
25+
private DataFlow::Node moduleMySQLdb(DataFlow::TypeTracker t) {
26+
t.start() and
27+
result = DataFlow::importNode("MySQLdb")
28+
or
29+
exists(DataFlow::TypeTracker t2 | result = moduleMySQLdb(t2).track(t2, t))
30+
}
31+
32+
/** Gets a reference to the `MySQLdb` module. */
33+
DataFlow::Node moduleMySQLdb() { result = moduleMySQLdb(DataFlow::TypeTracker::end()) }
34+
35+
class MySQLdb extends PEP249Module {
36+
MySQLdb() { this = moduleMySQLdb() }
37+
}
38+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `mysql-connector-python` package.
3+
* See
4+
* - https://dev.mysql.com/doc/connector-python/en/
5+
* - https://dev.mysql.com/doc/connector-python/en/connector-python-example-connecting.html
6+
*/
7+
8+
private import python
9+
private import experimental.dataflow.DataFlow
10+
private import experimental.dataflow.RemoteFlowSources
11+
private import experimental.semmle.python.Concepts
12+
private import PEP249
13+
14+
/**
15+
* Provides models for the `mysql-connector-python` package.
16+
* See
17+
* - https://dev.mysql.com/doc/connector-python/en/
18+
* - https://dev.mysql.com/doc/connector-python/en/connector-python-example-connecting.html
19+
*/
20+
module MysqlConnectorPython {
21+
// ---------------------------------------------------------------------------
22+
// mysql
23+
// ---------------------------------------------------------------------------
24+
/** Gets a reference to the `mysql` module. */
25+
private DataFlow::Node mysql(DataFlow::TypeTracker t) {
26+
t.start() and
27+
result = DataFlow::importNode("mysql")
28+
or
29+
exists(DataFlow::TypeTracker t2 | result = mysql(t2).track(t2, t))
30+
}
31+
32+
/** Gets a reference to the `mysql` module. */
33+
DataFlow::Node mysql() { result = mysql(DataFlow::TypeTracker::end()) }
34+
35+
/**
36+
* Gets a reference to the attribute `attr_name` of the `mysql` module.
37+
* WARNING: Only holds for a few predefined attributes.
38+
*/
39+
private DataFlow::Node mysql_attr(DataFlow::TypeTracker t, string attr_name) {
40+
attr_name in ["connector"] and
41+
(
42+
t.start() and
43+
result = DataFlow::importNode("mysql" + "." + attr_name)
44+
or
45+
t.startInAttr(attr_name) and
46+
result = mysql()
47+
)
48+
or
49+
// Due to bad performance when using normal setup with `mysql_attr(t2, attr_name).track(t2, t)`
50+
// we have inlined that code and forced a join
51+
exists(DataFlow::TypeTracker t2 |
52+
exists(DataFlow::StepSummary summary |
53+
mysql_attr_first_join(t2, attr_name, result, summary) and
54+
t = t2.append(summary)
55+
)
56+
)
57+
}
58+
59+
pragma[nomagic]
60+
private predicate mysql_attr_first_join(
61+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res, DataFlow::StepSummary summary
62+
) {
63+
DataFlow::StepSummary::step(mysql_attr(t2, attr_name), res, summary)
64+
}
65+
66+
/**
67+
* Gets a reference to the attribute `attr_name` of the `mysql` module.
68+
* WARNING: Only holds for a few predefined attributes.
69+
*/
70+
private DataFlow::Node mysql_attr(string attr_name) {
71+
result = mysql_attr(DataFlow::TypeTracker::end(), attr_name)
72+
}
73+
74+
/** Provides models for the `mysql` module. */
75+
module mysql {
76+
/**
77+
* The mysql.connector module
78+
* See https://dev.mysql.com/doc/connector-python/en/connector-python-example-connecting.html
79+
*/
80+
class MysqlConnector extends PEP249Module {
81+
MysqlConnector() { this = mysql_attr("connector") }
82+
}
83+
}
84+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Provides classes modeling PEP 249.
3+
* See https://www.python.org/dev/peps/pep-0249/.
4+
*/
5+
6+
private import python
7+
private import experimental.dataflow.DataFlow
8+
private import experimental.dataflow.RemoteFlowSources
9+
private import experimental.semmle.python.Concepts
10+
11+
/** A module implementing PEP 249. Extend this class for implementations. */
12+
abstract class PEP249Module extends DataFlow::Node { }
13+
14+
/** Gets a reference to a connect call. */
15+
private DataFlow::Node connect(DataFlow::TypeTracker t) {
16+
t.startInAttr("connect") and
17+
result instanceof PEP249Module
18+
or
19+
exists(DataFlow::TypeTracker t2 | result = connect(t2).track(t2, t))
20+
}
21+
22+
/** Gets a reference to a connect call. */
23+
DataFlow::Node connect() { result = connect(DataFlow::TypeTracker::end()) }
24+
25+
/**
26+
* Provides models for the `db.Connection` class
27+
*
28+
* See https://www.python.org/dev/peps/pep-0249/#connection-objects.
29+
*/
30+
module Connection {
31+
/**
32+
* A source of an instance of `db.Connection`.
33+
*
34+
* This can include instantiation of the class, return value from function
35+
* calls, or a special parameter that will be set when functions are called by external
36+
* libraries.
37+
*
38+
* Use `Connection::instance()` predicate to get references to instances of `db.Connection`.
39+
*
40+
* Extend this class if the module implementing PEP 249 offers more direct ways to obtain
41+
* a connection than going through `connect`.
42+
*/
43+
abstract class InstanceSource extends DataFlow::Node { }
44+
45+
/** A direct instantiation of `db.Connection`. */
46+
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
47+
override CallNode node;
48+
49+
ClassInstantiation() { node.getFunction() = connect().asCfgNode() }
50+
}
51+
52+
/** Gets a reference to an instance of `db.Connection`. */
53+
private DataFlow::Node instance(DataFlow::TypeTracker t) {
54+
t.start() and
55+
result instanceof InstanceSource
56+
or
57+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
58+
}
59+
60+
/** Gets a reference to an instance of `db.Connection`. */
61+
DataFlow::Node instance() { result = instance(DataFlow::TypeTracker::end()) }
62+
}
63+
64+
/**
65+
* Provides models for the `db.Connection.cursor` method.
66+
* See https://www.python.org/dev/peps/pep-0249/#cursor.
67+
*/
68+
module cursor {
69+
/** Gets a reference to the `db.connection.cursor` method. */
70+
private DataFlow::Node methodRef(DataFlow::TypeTracker t) {
71+
t.startInAttr("cursor") and
72+
result = Connection::instance()
73+
or
74+
exists(DataFlow::TypeTracker t2 | result = methodRef(t2).track(t2, t))
75+
}
76+
77+
/** Gets a reference to the `db.connection.cursor` metod. */
78+
DataFlow::Node methodRef() { result = methodRef(DataFlow::TypeTracker::end()) }
79+
80+
/** Gets a reference to a result of calling `db.connection.cursor`. */
81+
private DataFlow::Node methodResult(DataFlow::TypeTracker t) {
82+
t.start() and
83+
result.asCfgNode().(CallNode).getFunction() = methodRef().asCfgNode()
84+
or
85+
exists(DataFlow::TypeTracker t2 | result = methodResult(t2).track(t2, t))
86+
}
87+
88+
/** Gets a reference to a result of calling `db.connection.cursor`. */
89+
DataFlow::Node methodResult() { result = methodResult(DataFlow::TypeTracker::end()) }
90+
}
91+
92+
/**
93+
* Gets a reference to the `db.Connection.Cursor.execute` function.
94+
* See https://www.python.org/dev/peps/pep-0249/#id15.
95+
*/
96+
private DataFlow::Node execute(DataFlow::TypeTracker t) {
97+
t.startInAttr("execute") and
98+
result = cursor::methodResult()
99+
or
100+
exists(DataFlow::TypeTracker t2 | result = execute(t2).track(t2, t))
101+
}
102+
103+
/**
104+
* Gets a reference to the `db.Connection.Cursor.execute` function.
105+
* See https://www.python.org/dev/peps/pep-0249/#id15.
106+
*/
107+
DataFlow::Node execute() { result = execute(DataFlow::TypeTracker::end()) }
108+
109+
private class DbConnectionExecute extends SqlExecution::Range, DataFlow::CfgNode {
110+
override CallNode node;
111+
112+
DbConnectionExecute() { node.getFunction() = execute().asCfgNode() }
113+
114+
override DataFlow::Node getSql() {
115+
result.asCfgNode() in [node.getArg(0), node.getArgByName("sql")]
116+
}
117+
}

python/ql/test/experimental/library-tests/frameworks/mysql-connector-python/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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# taken from https://dev.mysql.com/doc/connector-python/en/connector-python-example-cursor-transaction.html
2+
from __future__ import print_function
3+
from datetime import date, datetime, timedelta
4+
import mysql.connector
5+
6+
cnx = mysql.connector.connect(user='scott', database='employees')
7+
cursor = cnx.cursor()
8+
9+
tomorrow = datetime.now().date() + timedelta(days=1)
10+
11+
add_employee = ("INSERT INTO employees "
12+
"(first_name, last_name, hire_date, gender, birth_date) "
13+
"VALUES (%s, %s, %s, %s, %s)")
14+
add_salary = ("INSERT INTO salaries "
15+
"(emp_no, salary, from_date, to_date) "
16+
"VALUES (%(emp_no)s, %(salary)s, %(from_date)s, %(to_date)s)")
17+
18+
data_employee = ('Geert', 'Vanderkelen', tomorrow, 'M', date(1977, 6, 14))
19+
20+
# Insert new employee
21+
cursor.execute(add_employee, data_employee) # $getSql=add_employee
22+
emp_no = cursor.lastrowid
23+
24+
# Insert salary information
25+
data_salary = {
26+
'emp_no': emp_no,
27+
'salary': 50000,
28+
'from_date': tomorrow,
29+
'to_date': date(9999, 1, 1),
30+
}
31+
cursor.execute(add_salary, data_salary) # $getSql=add_salary
32+
33+
# Make sure data is committed to the database
34+
cnx.commit()
35+
36+
cursor.close()
37+
cnx.close()

python/ql/test/experimental/library-tests/frameworks/mysqldb/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

0 commit comments

Comments
 (0)