Skip to content

Commit 802d9bd

Browse files
authored
Merge pull request github#5680 from mrthankyou/python-use-sqlalchemy
Python: Add SqlAlchemy model
2 parents f6f9c8a + 9e01338 commit 802d9bd

File tree

8 files changed

+258
-0
lines changed

8 files changed

+258
-0
lines changed

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,36 @@ class LDAPEscape extends DataFlow::Node {
146146
*/
147147
DataFlow::Node getAnInput() { result = range.getAnInput() }
148148
}
149+
150+
/** Provides classes for modeling SQL sanitization libraries. */
151+
module SQLEscape {
152+
/**
153+
* A data-flow node that collects functions that escape SQL statements.
154+
*
155+
* Extend this class to model new APIs. If you want to refine existing API models,
156+
* extend `SQLEscape` instead.
157+
*/
158+
abstract class Range extends DataFlow::Node {
159+
/**
160+
* Gets the argument containing the raw SQL statement.
161+
*/
162+
abstract DataFlow::Node getAnInput();
163+
}
164+
}
165+
166+
/**
167+
* A data-flow node that collects functions escaping SQL statements.
168+
*
169+
* Extend this class to refine existing API models. If you want to model new APIs,
170+
* extend `SQLEscape::Range` instead.
171+
*/
172+
class SQLEscape extends DataFlow::Node {
173+
SQLEscape::Range range;
174+
175+
SQLEscape() { this = range }
176+
177+
/**
178+
* Gets the argument containing the raw SQL statement.
179+
*/
180+
DataFlow::Node getAnInput() { result = range.getAnInput() }
181+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the 'SqlAlchemy' package.
3+
* See https://pypi.org/project/SQLAlchemy/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.TaintTracking
9+
private import semmle.python.ApiGraphs
10+
private import semmle.python.Concepts
11+
private import experimental.semmle.python.Concepts
12+
13+
private module SqlAlchemy {
14+
/**
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
18+
*/
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+
}
23+
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+
}
31+
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()
38+
}
39+
40+
/**
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
46+
*/
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()
57+
}
58+
59+
override DataFlow::Node getSql() { result = this.getArg(0) }
60+
}
61+
62+
/**
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
66+
*/
67+
private class SqlAlchemyScalarCall extends DataFlow::CallCfgNode, SqlExecution::Range {
68+
SqlAlchemyScalarCall() {
69+
this =
70+
[getSqlAlchemySessionInstance(), getSqlAlchemyEngineInstance()]
71+
.getMember("scalar")
72+
.getACall()
73+
}
74+
75+
override DataFlow::Node getSql() { result = this.getArg(0) }
76+
}
77+
78+
/**
79+
* A call on a Query object
80+
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
81+
*/
82+
private class SqlAlchemyQueryCall extends DataFlow::CallCfgNode, SqlExecution::Range {
83+
SqlAlchemyQueryCall() {
84+
this =
85+
getSqlAlchemyQueryInstance()
86+
.getMember(any(SqlAlchemyVulnerableMethodNames methodName))
87+
.getACall()
88+
}
89+
90+
override DataFlow::Node getSql() { result = this.getArg(0) }
91+
}
92+
93+
/**
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
97+
*/
98+
private class SqlAlchemyVulnerableMethodNames extends string {
99+
SqlAlchemyVulnerableMethodNames() { this in ["filter", "filter_by", "group_by", "order_by"] }
100+
}
101+
102+
/**
103+
* Additional taint-steps for `sqlalchemy.text()`
104+
*
105+
* See https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text
106+
* See https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.TextClause
107+
*/
108+
class SqlAlchemyTextAdditionalTaintSteps extends TaintTracking::AdditionalTaintStep {
109+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
110+
exists(DataFlow::CallCfgNode call |
111+
(
112+
call = API::moduleImport("sqlalchemy").getMember("text").getACall()
113+
or
114+
call = API::moduleImport("sqlalchemy").getMember("sql").getMember("text").getACall()
115+
or
116+
call =
117+
API::moduleImport("sqlalchemy")
118+
.getMember("sql")
119+
.getMember("expression")
120+
.getMember("text")
121+
.getACall()
122+
or
123+
call =
124+
API::moduleImport("sqlalchemy")
125+
.getMember("sql")
126+
.getMember("expression")
127+
.getMember("TextClause")
128+
.getACall()
129+
) and
130+
nodeFrom in [call.getArg(0), call.getArgByName("text")] and
131+
nodeTo = call
132+
)
133+
}
134+
}
135+
136+
/**
137+
* Gets a reference to `sqlescapy.sqlescape`.
138+
*
139+
* See https://pypi.org/project/sqlescapy/
140+
*/
141+
class SQLEscapySanitizerCall extends DataFlow::CallCfgNode, SQLEscape::Range {
142+
SQLEscapySanitizerCall() {
143+
this = API::moduleImport("sqlescapy").getMember("sqlescape").getACall()
144+
}
145+
146+
override DataFlow::Node getAnInput() { result = this.getArg(0) }
147+
}
148+
}

python/ql/test/experimental/library-tests/frameworks/sqlalchemy/ConceptsTest.expected

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import python
2+
import experimental.meta.ConceptsTest
3+
import experimental.semmle.python.frameworks.SqlAlchemy
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
argumentToEnsureNotTaintedNotMarkedAsSpurious
2+
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
3+
failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import experimental.meta.InlineTaintTest
2+
import experimental.semmle.python.frameworks.SqlAlchemy
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import sqlalchemy
2+
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
3+
from sqlalchemy.ext.declarative import declarative_base
4+
from sqlalchemy.pool import StaticPool
5+
from sqlalchemy.orm import relationship, backref, sessionmaker, joinedload
6+
from sqlalchemy.sql import text
7+
8+
engine = create_engine(
9+
'sqlite:///:memory:',
10+
echo=True,
11+
connect_args={"check_same_thread": False},
12+
poolclass=StaticPool
13+
)
14+
15+
Base = declarative_base()
16+
17+
class User(Base):
18+
__tablename__ = 'users'
19+
20+
id = Column(Integer, primary_key=True)
21+
name = Column(String)
22+
23+
Base.metadata.create_all(engine)
24+
25+
Session = sessionmaker(bind=engine)
26+
session = Session()
27+
28+
ed_user = User(name='ed')
29+
ed_user2 = User(name='george')
30+
31+
session.add(ed_user)
32+
session.add(ed_user2)
33+
34+
session.commit()
35+
36+
# Injection without requiring the text() taint-step
37+
session.query(User).filter_by(name="some sql") # $ MISSING: getSql="some sql"
38+
session.scalar("some sql") # $ getSql="some sql"
39+
engine.scalar("some sql") # $ getSql="some sql"
40+
session.execute("some sql") # $ getSql="some sql"
41+
42+
with engine.connect() as connection:
43+
connection.execute("some sql") # $ getSql="some sql"
44+
45+
with engine.begin() as connection:
46+
connection.execute("some sql") # $ getSql="some sql"
47+
48+
# Injection requiring the text() taint-step
49+
t = text("some sql")
50+
session.query(User).filter(t) # $ getSql=t
51+
session.query(User).group_by(User.id).having(t) # $ getSql=User.id MISSING: getSql=t
52+
session.query(User).group_by(t).first() # $ getSql=t
53+
session.query(User).order_by(t).first() # $ getSql=t
54+
55+
query = select(User).where(User.name == t) # $ MISSING: getSql=t
56+
with engine.connect() as conn:
57+
conn.execute(query) # $ getSql=query
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import sqlalchemy
2+
3+
def test_taint():
4+
ts = TAINTED_STRING
5+
6+
ensure_tainted(
7+
ts, # $ tainted
8+
sqlalchemy.text(ts), # $ tainted
9+
sqlalchemy.sql.text(ts),# $ tainted
10+
sqlalchemy.sql.expression.text(ts),# $ tainted
11+
sqlalchemy.sql.expression.TextClause(ts),# $ tainted
12+
)

0 commit comments

Comments
 (0)