Skip to content

Commit 0ea1b85

Browse files
authored
Add PEP249 connection tracking through class attribute wrappers
Introduce two new Connection::InstanceSource subclasses in PEP249.qll: - ConnectionGetterAttributeRead: recognises self._conn reads inside getter methods of classes whose __init__ stores a connect() call in that attribute. The AttrRead node coincides with the return node, so the existing TypeTracker returnStep propagates the connection type to all call sites automatically. - ConnectionConstructorAttributeRead: recognises ClassName()._conn direct attribute reads on constructor-call results. Both classes share the classStoresConnectionInInit helper predicate that checks for the self.attr = dbapi.connect() store pattern in __init__. Also adds test cases for the new patterns in the hdbcli test suite and a change note.
1 parent 0ef59df commit 0ea1b85

3 files changed

Lines changed: 119 additions & 0 deletions

File tree

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import semmle.python.dataflow.new.DataFlow
88
private import semmle.python.dataflow.new.RemoteFlowSources
99
private import semmle.python.Concepts
1010
private import semmle.python.ApiGraphs
11+
private import semmle.python.dataflow.new.internal.DataFlowDispatch as DataFlowDispatch
1112

1213
/**
1314
* Provides classes modeling database interfaces following PEP 249.
@@ -212,6 +213,74 @@ module PEP249 {
212213
ConnectCall() { this.getFunction() = connect() }
213214
}
214215

216+
/**
217+
* Holds if class `cls` stores a PEP 249 database connection to `self.<attrName>`
218+
* in its `__init__` method, via a direct call to a `connect` function.
219+
*/
220+
private predicate classStoresConnectionInInit(Class cls, string attrName) {
221+
exists(Function init, DataFlow::AttrWrite store |
222+
cls.getAMethod() = init and
223+
init.getName() = "__init__" and
224+
store.getAttributeName() = attrName and
225+
store.getObject().asCfgNode().getNode().(Name).getVariable() =
226+
init.getArg(0).asName().getVariable() and
227+
store.getValue() instanceof ConnectCall
228+
)
229+
}
230+
231+
/**
232+
* A read of a connection-holding attribute within a method of a class whose
233+
* `__init__` stores a PEP 249 connection in that attribute.
234+
*
235+
* This recognises patterns such as:
236+
* ```python
237+
* class Wrapper:
238+
* def __init__(self):
239+
* self._conn = dbapi.connect(...)
240+
* def get_connection(self):
241+
* return self._conn # <-- recognised as a connection source
242+
* ```
243+
* Because the `AttrRead` node for `self._conn` inside `get_connection` is
244+
* also the `ExtractedReturnNode` for that statement, the existing TypeTracker
245+
* `returnStep` automatically propagates the connection type to all call sites
246+
* of `get_connection`.
247+
*/
248+
private class ConnectionGetterAttributeRead extends InstanceSource, DataFlow::AttrRead {
249+
ConnectionGetterAttributeRead() {
250+
exists(Class cls, Function method, string attrName |
251+
classStoresConnectionInInit(cls, attrName) and
252+
cls.getAMethod() = method and
253+
method.getName() != "__init__" and
254+
this.getAttributeName() = attrName and
255+
this.getObject().asCfgNode().getNode().(Name).getVariable() =
256+
method.getArg(0).asName().getVariable()
257+
)
258+
}
259+
}
260+
261+
/**
262+
* An attribute access on a constructor-call result that directly reads the
263+
* connection-holding attribute.
264+
*
265+
* This recognises patterns such as:
266+
* ```python
267+
* class Wrapper:
268+
* def __init__(self):
269+
* self._conn = dbapi.connect(...)
270+
*
271+
* conn = Wrapper()._conn # <-- recognised as a connection source
272+
* ```
273+
*/
274+
private class ConnectionConstructorAttributeRead extends InstanceSource, DataFlow::AttrRead {
275+
ConnectionConstructorAttributeRead() {
276+
exists(Class cls, string attrName |
277+
classStoresConnectionInInit(cls, attrName) and
278+
this.getAttributeName() = attrName and
279+
DataFlowDispatch::resolveClassCall(this.getObject().asCfgNode().(CallNode), cls)
280+
)
281+
}
282+
}
283+
215284
/** Gets a reference to a database connection (following PEP 249). */
216285
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
217286
t.start() and
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Improved detection of SQL injection and other PEP 249 database-related vulnerabilities when a database connection is stored in a class instance attribute and accessed through a getter method or direct attribute read. For example, patterns like `self._conn = dbapi.connect(...)` in `__init__` followed by `return self._conn` in a getter method, or `MyClass()._conn`, are now correctly recognised as PEP 249 connection sources.

python/ql/test/library-tests/frameworks/hdbcli/pep249.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,49 @@
77
cursor.executemany("some sql", (42,)) # $ getSql="some sql"
88

99
cursor.close()
10+
11+
12+
# ---------------------------------------------------------------------------
13+
# Connection stored in a class attribute and accessed via various patterns
14+
# ---------------------------------------------------------------------------
15+
16+
17+
class WrapperA:
18+
def __init__(self):
19+
self._conn = dbapi.connect(address="hostname", port=300, user="username", pass_arg="testpass")
20+
21+
def get_connection(self):
22+
return self._conn
23+
24+
25+
# Getter called on a fresh constructor result
26+
conn_a1 = WrapperA().get_connection()
27+
cursor_a1 = conn_a1.cursor()
28+
cursor_a1.execute("some sql", (42,)) # $ getSql="some sql"
29+
30+
# Getter called via a stored wrapper instance
31+
wrapper_instance = WrapperA()
32+
conn_a2 = wrapper_instance.get_connection()
33+
cursor_a2 = conn_a2.cursor()
34+
cursor_a2.execute("some sql", (42,)) # $ getSql="some sql"
35+
36+
# Direct attribute access on a fresh constructor result
37+
conn_b = WrapperA()._conn
38+
cursor_b = conn_b.cursor()
39+
cursor_b.execute("some sql", (42,)) # $ getSql="some sql"
40+
41+
42+
class WrapperB:
43+
"""Stores the connection under a different attribute name."""
44+
45+
def __init__(self):
46+
self._hana = dbapi.connect(address="hostname", port=300, user="username", pass_arg="testpass")
47+
48+
def cursor(self):
49+
return self._hana.cursor()
50+
51+
52+
# Direct attribute access on a stored instance (mirrors hdb_con3 in the issue)
53+
conn_c = WrapperB()._hana
54+
cursor_c = conn_c.cursor()
55+
cursor_c.execute("some sql", (42,)) # $ getSql="some sql"

0 commit comments

Comments
 (0)