Skip to content

Commit de926dc

Browse files
authored
Merge pull request #7085 from yoff/python/model-aiopg
Python: model aiopg
2 parents 0f08605 + 92a7114 commit de926dc

File tree

7 files changed

+180
-0
lines changed

7 files changed

+180
-0
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ Python built-in support
183183
pydantic, Utility library
184184
yarl, Utility library
185185
aioch, Database
186+
aiopg, Database
186187
asyncpg, Database
187188
clickhouse-driver, Database
188189
mysql-connector-python, Database
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 `aiopg` for sinks executing SQL.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// `docs/codeql/support/reusables/frameworks.rst`
77
private import semmle.python.frameworks.Aioch
88
private import semmle.python.frameworks.Aiohttp
9+
private import semmle.python.frameworks.Aiopg
910
private import semmle.python.frameworks.Asyncpg
1011
private import semmle.python.frameworks.ClickhouseDriver
1112
private import semmle.python.frameworks.Cryptodome
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `aiopg` PyPI package.
3+
* See
4+
* - https://aiopg.readthedocs.io/en/stable/index.html
5+
* - https://pypi.org/project/aiopg/
6+
*/
7+
8+
private import python
9+
private import semmle.python.dataflow.new.DataFlow
10+
private import semmle.python.Concepts
11+
private import semmle.python.ApiGraphs
12+
13+
/** Provides models for the `aiopg` PyPI package. */
14+
private module Aiopg {
15+
private import semmle.python.internal.Awaited
16+
17+
/**
18+
* A `ConectionPool` is created when the result of `aiopg.create_pool()` is awaited.
19+
* See https://aiopg.readthedocs.io/en/stable/core.html#pool
20+
*/
21+
API::Node connectionPool() {
22+
result = API::moduleImport("aiopg").getMember("create_pool").getReturn().getAwaited()
23+
}
24+
25+
/**
26+
* A `Connection` is created when
27+
* - the result of `aiopg.connect()` is awaited.
28+
* - the result of calling `aquire` on a `ConnectionPool` is awaited.
29+
* See https://aiopg.readthedocs.io/en/stable/core.html#connection
30+
*/
31+
API::Node connection() {
32+
result = API::moduleImport("aiopg").getMember("connect").getReturn().getAwaited()
33+
or
34+
result = connectionPool().getMember("acquire").getReturn().getAwaited()
35+
}
36+
37+
/**
38+
* A `Cursor` is created when
39+
* - the result of calling `cursor` on a `ConnectionPool` is awaited.
40+
* - the result of calling `cursor` on a `Connection` is awaited.
41+
* See https://aiopg.readthedocs.io/en/stable/core.html#cursor
42+
*/
43+
API::Node cursor() {
44+
result = connectionPool().getMember("cursor").getReturn().getAwaited()
45+
or
46+
result = connection().getMember("cursor").getReturn().getAwaited()
47+
}
48+
49+
/**
50+
* Calling `execute` on a `Cursor` constructs a query.
51+
* See https://aiopg.readthedocs.io/en/stable/core.html#aiopg.Cursor.execute
52+
*/
53+
class CursorExecuteCall extends SqlConstruction::Range, DataFlow::CallCfgNode {
54+
CursorExecuteCall() { this = cursor().getMember("execute").getACall() }
55+
56+
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("operation")] }
57+
}
58+
59+
/**
60+
* This is only needed to connect the argument to the execute call with the subsequnt awaiting.
61+
* It should be obsolete once we have `API::CallNode` available.
62+
*/
63+
private DataFlow::TypeTrackingNode cursorExecuteCall(DataFlow::TypeTracker t, DataFlow::Node sql) {
64+
// cursor created from connection
65+
t.start() and
66+
sql = result.(CursorExecuteCall).getSql()
67+
or
68+
exists(DataFlow::TypeTracker t2 | result = cursorExecuteCall(t2, sql).track(t2, t))
69+
}
70+
71+
DataFlow::Node cursorExecuteCall(DataFlow::Node sql) {
72+
cursorExecuteCall(DataFlow::TypeTracker::end(), sql).flowsTo(result)
73+
}
74+
75+
/**
76+
* Awaiting the result of calling `execute` executes the query.
77+
* See https://aiopg.readthedocs.io/en/stable/core.html#aiopg.Cursor.execute
78+
*/
79+
class AwaitedCursorExecuteCall extends SqlExecution::Range {
80+
DataFlow::Node sql;
81+
82+
AwaitedCursorExecuteCall() { this = awaited(cursorExecuteCall(sql)) }
83+
84+
override DataFlow::Node getSql() { result = sql }
85+
}
86+
87+
/**
88+
* An `Engine` is created when the result of calling `aiopg.sa.create_engine` is awaited.
89+
* See https://aiopg.readthedocs.io/en/stable/sa.html#engine
90+
*/
91+
API::Node engine() {
92+
result =
93+
API::moduleImport("aiopg").getMember("sa").getMember("create_engine").getReturn().getAwaited()
94+
}
95+
96+
/**
97+
* A `SAConnection` is created when the result of calling `aquire` on an `Engine` is awaited.
98+
* See https://aiopg.readthedocs.io/en/stable/sa.html#connection
99+
*/
100+
API::Node saConnection() { result = engine().getMember("acquire").getReturn().getAwaited() }
101+
102+
/**
103+
* Calling `execute` on a `SAConnection` constructs a query.
104+
* See https://aiopg.readthedocs.io/en/stable/sa.html#aiopg.sa.SAConnection.execute
105+
*/
106+
class SAConnectionExecuteCall extends SqlConstruction::Range, DataFlow::CallCfgNode {
107+
SAConnectionExecuteCall() { this = saConnection().getMember("execute").getACall() }
108+
109+
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("query")] }
110+
}
111+
112+
/**
113+
* This is only needed to connect the argument to the execute call with the subsequnt awaiting.
114+
* It should be obsolete once we have `API::CallNode` available.
115+
*/
116+
private DataFlow::TypeTrackingNode saConnectionExecuteCall(
117+
DataFlow::TypeTracker t, DataFlow::Node sql
118+
) {
119+
// saConnection created from engine
120+
t.start() and
121+
sql = result.(SAConnectionExecuteCall).getSql()
122+
or
123+
exists(DataFlow::TypeTracker t2 | result = saConnectionExecuteCall(t2, sql).track(t2, t))
124+
}
125+
126+
DataFlow::Node saConnectionExecuteCall(DataFlow::Node sql) {
127+
saConnectionExecuteCall(DataFlow::TypeTracker::end(), sql).flowsTo(result)
128+
}
129+
130+
/**
131+
* Awaiting the result of calling `execute` executes the query.
132+
* See https://aiopg.readthedocs.io/en/stable/sa.html#aiopg.sa.SAConnection.execute
133+
*/
134+
class AwaitedSAConnectionExecuteCall extends SqlExecution::Range {
135+
DataFlow::Node sql;
136+
137+
AwaitedSAConnectionExecuteCall() { this = awaited(saConnectionExecuteCall(sql)) }
138+
139+
override DataFlow::Node getSql() { result = sql }
140+
}
141+
}

python/ql/test/library-tests/frameworks/aiopg/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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import aiopg
2+
3+
# Only a cursor can execute sql.
4+
async def test_cursor():
5+
# Create connection directly
6+
conn = await aiopg.connect()
7+
cur = await conn.cursor()
8+
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
9+
10+
# Create connection via pool
11+
async with aiopg.create_pool() as pool:
12+
# Create Cursor via Connection
13+
async with pool.acquire() as conn:
14+
async with conn.cursor() as cur:
15+
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
16+
17+
# Create Cursor directly
18+
async with pool.cursor() as cur:
19+
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
20+
21+
# variants using as few `async with` as possible
22+
pool = await aiopg.create_pool()
23+
conn = await pool.acquire()
24+
cur = await conn.cursor()
25+
await cur.execute("sql") # $ getSql="sql" constructedSql="sql"
26+
27+
# Test SQLAlchemy integration
28+
from aiopg.sa import create_engine
29+
30+
async def test_engine():
31+
engine = await create_engine()
32+
conn = await engine.acquire()
33+
await conn.execute("sql") # $ getSql="sql" constructedSql="sql"

0 commit comments

Comments
 (0)