mirror of
https://github.com/github/codeql.git
synced 2026-04-30 11:15:13 +02:00
Python: Add query to handle SQLAlchemy TextClause Injection
instead of doing this via taint-steps. See description in code/tests.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
The <code>TextClause</code> class in the <code>SQLAlchemy</code> PyPI package represents
|
||||
a textual SQL string directly. If user-input is added to it without sufficient
|
||||
sanitization, a user may be able to run malicious database queries, since the
|
||||
<code>TextClause</code> is inserted directly into the final SQL.
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Don't allow user-input to be added to a <code>TextClause</code>, instead construct your
|
||||
full query with constructs from the ORM, or use query parameters for user-input.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
In the following snippet, a user is fetched from the database using three
|
||||
different queries.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In the first case, the final query string is built by directly including a user-supplied
|
||||
input. The parameter may include quote characters, so this code is vulnerable to a SQL
|
||||
injection attack.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In the second case, the query is built using ORM models, but part of it is using a
|
||||
<code>TextClause</code> directly including a user-supplied input. Since the
|
||||
<code>TextClause</code> is inserted directly into the final SQL, this code is vulnerable
|
||||
to a SQL injection attack.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In the third case, the query is built fully using the ORM models, so in the end, the
|
||||
user-supplied input will passed passed to the database using query parameters. The
|
||||
database connector library will take care of escaping and inserting quotes as needed.
|
||||
</p>
|
||||
|
||||
<sample src="examples/sqlalchemy_textclause_injection.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li><a href="https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text.params.text">Official documentation of the text parameter</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @name SQLAlchemy TextClause built from user-controlled sources
|
||||
* @description Building a TextClause query from user-controlled sources is vulnerable to insertion of
|
||||
* malicious SQL code by the user.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 8.8
|
||||
* @precision high
|
||||
* @id py/sqlalchemy-textclause-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-089
|
||||
* external/owasp/owasp-a1
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.security.dataflow.SQLAlchemyTextClause
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from SQLAlchemyTextClause::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
"This SQLAlchemy TextClause depends on $@, which could lead to SQL injection.", source.getNode(),
|
||||
"a user-provided value"
|
||||
@@ -0,0 +1,34 @@
|
||||
from flask import Flask, request
|
||||
import sqlalchemy
|
||||
import sqlalchemy.orm
|
||||
|
||||
app = Flask(__name__)
|
||||
engine = sqlalchemy.create_engine(...)
|
||||
Base = sqlalchemy.orm.declarative_base()
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
||||
username = sqlalchemy.Column(sqlalchemy.String)
|
||||
|
||||
|
||||
@app.route("/users/<username>")
|
||||
def show_user(username):
|
||||
session = sqlalchemy.orm.Session(engine)
|
||||
|
||||
# BAD, normal SQL injection
|
||||
stmt = sqlalchemy.text("SELECT * FROM users WHERE username = '{}'".format(username))
|
||||
results = session.execute(stmt).fetchall()
|
||||
|
||||
# BAD, allows SQL injection
|
||||
username_formatted_for_sql = sqlalchemy.text("'{}'".format(username))
|
||||
stmt = sqlalchemy.select(User).where(User.username == username_formatted_for_sql)
|
||||
results = session.execute(stmt).scalars().all()
|
||||
|
||||
# GOOD, does not allow for SQL injection
|
||||
stmt = sqlalchemy.select(User).where(User.username == username)
|
||||
results = session.execute(stmt).scalars().all()
|
||||
|
||||
...
|
||||
Reference in New Issue
Block a user