mirror of
https://github.com/github/codeql.git
synced 2026-04-27 01:35: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,2 @@
|
||||
lgtm,codescanning
|
||||
* Introduced a new query _SQLAlchemy TextClause built from user-controlled sources_ (`py/sqlalchemy-textclause-injection`) to alert if user-input is added to a TextClause from SQLAlchemy, since that can lead to SQL injection.
|
||||
@@ -15,12 +15,14 @@ private import semmle.python.Concepts
|
||||
private import semmle.python.frameworks.PEP249::PEP249 as PEP249
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for the `SQLAlchemy` PyPI package.
|
||||
* See
|
||||
* - https://pypi.org/project/SQLAlchemy/
|
||||
* - https://docs.sqlalchemy.org/en/14/index.html
|
||||
*/
|
||||
private module SqlAlchemy {
|
||||
module SqlAlchemy {
|
||||
/**
|
||||
* Provides models for the `sqlalchemy.engine.Engine` and `sqlalchemy.future.Engine` classes.
|
||||
*
|
||||
@@ -279,80 +281,62 @@ private module SqlAlchemy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional taint-steps for `sqlalchemy.text()`
|
||||
* Provides models for the `sqlalchemy.sql.expression.TextClause` class,
|
||||
* which represents a textual SQL string directly.
|
||||
*
|
||||
* See https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text
|
||||
* See https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.TextClause
|
||||
* ```py
|
||||
* session.query(For14).filter_by(description=sqlalchemy.text(f"'{user_input}'")).all()
|
||||
* ```
|
||||
*
|
||||
* Initially I wanted to add lots of additional taint steps for such that the normal
|
||||
* SQL injection query would be able to find cases as the one above where an ORM query
|
||||
* includes a TextClause that includes user-input directly... But that presented 2
|
||||
* problems:
|
||||
*
|
||||
* - which part of the query construction above should be marked as SQL to fit our
|
||||
* `SqlExecution` concept. Nothing really fits this well, since all the SQL
|
||||
* execution happens under the hood.
|
||||
* - This would require a LOT of modeling for these additional taint steps, since
|
||||
* there are many many constructs we would need to have models for. (see the 2
|
||||
* examples below)
|
||||
*
|
||||
* So instead we flag user-input to a TextClause with its' own query
|
||||
* (`py/sqlalchemy-textclause-injection`). And so we don't highlight any parts of an
|
||||
* ORM constructed query such as these as containing SQL, and don't need the additional
|
||||
* taint steps either.
|
||||
*
|
||||
* See
|
||||
* - https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.TextClause.
|
||||
* - https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text
|
||||
*/
|
||||
class SqlAlchemyTextAdditionalTaintSteps extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(DataFlow::CallCfgNode call |
|
||||
(
|
||||
call = API::moduleImport("sqlalchemy").getMember("text").getACall()
|
||||
or
|
||||
call = API::moduleImport("sqlalchemy").getMember("sql").getMember("text").getACall()
|
||||
or
|
||||
call =
|
||||
API::moduleImport("sqlalchemy")
|
||||
.getMember("sql")
|
||||
.getMember("expression")
|
||||
.getMember("text")
|
||||
.getACall()
|
||||
or
|
||||
call =
|
||||
API::moduleImport("sqlalchemy")
|
||||
.getMember("sql")
|
||||
.getMember("expression")
|
||||
.getMember("TextClause")
|
||||
.getACall()
|
||||
) and
|
||||
nodeFrom in [call.getArg(0), call.getArgByName("text")] and
|
||||
nodeTo = call
|
||||
)
|
||||
module TextClause {
|
||||
/**
|
||||
* A construction of a `sqlalchemy.sql.expression.TextClause`, which represents a
|
||||
* textual SQL string directly.
|
||||
*/
|
||||
class TextClauseConstruction extends DataFlow::CallCfgNode {
|
||||
TextClauseConstruction() {
|
||||
this = API::moduleImport("sqlalchemy").getMember("text").getACall()
|
||||
or
|
||||
this = API::moduleImport("sqlalchemy").getMember("sql").getMember("text").getACall()
|
||||
or
|
||||
this =
|
||||
API::moduleImport("sqlalchemy")
|
||||
.getMember("sql")
|
||||
.getMember("expression")
|
||||
.getMember("text")
|
||||
.getACall()
|
||||
or
|
||||
this =
|
||||
API::moduleImport("sqlalchemy")
|
||||
.getMember("sql")
|
||||
.getMember("expression")
|
||||
.getMember("TextClause")
|
||||
.getACall()
|
||||
}
|
||||
|
||||
/** Gets the argument that specifies the SQL text. */
|
||||
DataFlow::Node getTextArg() { result in [this.getArg(0), this.getArgByName("text")] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private module OldModeling {
|
||||
/**
|
||||
* Returns an instantization of a SqlAlchemy Session object.
|
||||
* See https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session and
|
||||
* https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.sessionmaker
|
||||
*/
|
||||
private API::Node getSqlAlchemySessionInstance() {
|
||||
result = API::moduleImport("sqlalchemy.orm").getMember("Session").getReturn() or
|
||||
result = API::moduleImport("sqlalchemy.orm").getMember("sessionmaker").getReturn().getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instantization of a SqlAlchemy Query object.
|
||||
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
|
||||
*/
|
||||
private API::Node getSqlAlchemyQueryInstance() {
|
||||
result = getSqlAlchemySessionInstance().getMember("query").getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* A call on a Query object
|
||||
* See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query
|
||||
*/
|
||||
private class SqlAlchemyQueryCall extends DataFlow::CallCfgNode, SqlExecution::Range {
|
||||
SqlAlchemyQueryCall() {
|
||||
this =
|
||||
getSqlAlchemyQueryInstance()
|
||||
.getMember(any(SqlAlchemyVulnerableMethodNames methodName))
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() { result = this.getArg(0) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a list of methods vulnerable to sql injection.
|
||||
*
|
||||
* See https://github.com/jty-team/codeql/pull/2#issue-611592361
|
||||
*/
|
||||
private class SqlAlchemyVulnerableMethodNames extends string {
|
||||
SqlAlchemyVulnerableMethodNames() { this in ["filter", "filter_by", "group_by", "order_by"] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Provides a taint-tracking configuration for detecting "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `SQLAlchemyTextClause::Configuration` is needed, otherwise
|
||||
* `SQLAlchemyTextClauseCustomizations` should be imported instead.
|
||||
*/
|
||||
|
||||
private import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
|
||||
/**
|
||||
* Provides a taint-tracking configuration for detecting "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*/
|
||||
module SQLAlchemyTextClause {
|
||||
import SQLAlchemyTextClauseCustomizations::SQLAlchemyTextClause
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "SQLAlchemyTextClause" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof SanitizerGuard
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
* "SQLAlchemy TextClause injection"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.SqlAlchemy
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
* "SQLAlchemy TextClause injection"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
module SQLAlchemyTextClause {
|
||||
/**
|
||||
* A data flow source for "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer guard for "SQLAlchemy TextClause injection" vulnerabilities.
|
||||
*/
|
||||
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
|
||||
|
||||
/**
|
||||
* A source of remote user input, considered as a flow source.
|
||||
*/
|
||||
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
|
||||
|
||||
/**
|
||||
* The text argument of a SQLAlchemy TextClause construction, considered as a flow sink.
|
||||
*/
|
||||
class TextArgAsSink extends Sink {
|
||||
TextArgAsSink() { this = any(SqlAlchemy::TextClause::TextClauseConstruction tcc).getTextArg() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
...
|
||||
@@ -1,3 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
import experimental.semmle.python.frameworks.SqlAlchemy
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
import experimental.semmle.python.frameworks.SqlAlchemy
|
||||
|
||||
@@ -47,10 +47,10 @@ with engine.begin() as connection:
|
||||
|
||||
# Injection requiring the text() taint-step
|
||||
t = text("some sql")
|
||||
session.query(User).filter(t) # $ getSql=t
|
||||
session.query(User).group_by(User.id).having(t) # $ getSql=User.id MISSING: getSql=t
|
||||
session.query(User).group_by(t).first() # $ getSql=t
|
||||
session.query(User).order_by(t).first() # $ getSql=t
|
||||
session.query(User).filter(t)
|
||||
session.query(User).group_by(User.id).having(t)
|
||||
session.query(User).group_by(t).first()
|
||||
session.query(User).order_by(t).first()
|
||||
|
||||
query = select(User).where(User.name == t) # $ MISSING: getSql=t
|
||||
with engine.connect() as conn:
|
||||
|
||||
@@ -189,57 +189,41 @@ text_foo = sqlalchemy.text("'FOO'")
|
||||
#
|
||||
# which is then called without any arguments
|
||||
assert session.query(For14).filter_by(description="'FOO'").all() == []
|
||||
query = session.query(For14).filter_by(description=text_foo) # $ MISSING: getSql=text_foo
|
||||
query = session.query(For14).filter_by(description=text_foo)
|
||||
assert query.all() == []
|
||||
|
||||
# Initially I wanted to add lots of additional taint steps such that the normal SQL
|
||||
# injection query would find these cases where an ORM query includes a TextClause that
|
||||
# includes user-input directly... But that presented 2 problems:
|
||||
#
|
||||
# - which part of the query construction above should be marked as SQL to fit our
|
||||
# `SqlExecution` concept. Nothing really fits this well, since all the SQL execution
|
||||
# happens under the hood.
|
||||
# - This would require a LOT of modeling for these additional taint steps, since there
|
||||
# are many many constructs we would need to have models for. (see the 2 examples below)
|
||||
#
|
||||
# So instead we flag user-input to a TextClause with its' own query. And so we don't
|
||||
# highlight any parts of an ORM constructed query such as these as containing SQL.
|
||||
|
||||
# `filter` provides more general filtering
|
||||
# see https://docs.sqlalchemy.org/en/14/orm/tutorial.html#common-filter-operators
|
||||
# and https://docs.sqlalchemy.org/en/14/orm/query.html#sqlalchemy.orm.Query.filter
|
||||
assert session.query(For14).filter(For14.description == "'FOO'").all() == []
|
||||
query = session.query(For14).filter(For14.description == text_foo) # $ MISSING: getSql=text_foo
|
||||
query = session.query(For14).filter(For14.description == text_foo)
|
||||
assert query.all() == []
|
||||
|
||||
assert session.query(For14).filter(For14.description.like("'FOO'")).all() == []
|
||||
query = session.query(For14).filter(For14.description.like(text_foo)) # $ MISSING: getSql=text_foo
|
||||
query = session.query(For14).filter(For14.description.like(text_foo))
|
||||
assert query.all() == []
|
||||
|
||||
# `where` is alias for `filter`
|
||||
assert session.query(For14).where(For14.description == "'FOO'").all() == []
|
||||
query = session.query(For14).where(For14.description == text_foo) # $ MISSING: getSql=text_foo
|
||||
assert query.all() == []
|
||||
|
||||
|
||||
# not possible to do SQL injection on `.get`
|
||||
try:
|
||||
session.query(For14).get(text_foo)
|
||||
except sqlalchemy.exc.InterfaceError:
|
||||
pass
|
||||
|
||||
# group_by
|
||||
assert session.query(For14).group_by(For14.description).all() != []
|
||||
query = session.query(For14).group_by(text_foo) # $ MISSING: getSql=text_foo
|
||||
assert query.all() != []
|
||||
|
||||
# having (only used in connection with group_by)
|
||||
assert session.query(For14).group_by(For14.description).having(
|
||||
sqlalchemy.func.count(For14.id) > 2
|
||||
).all() == []
|
||||
query = session.query(For14).group_by(For14.description).having(text_foo) # $ MISSING: getSql=text_foo
|
||||
assert query.all() == []
|
||||
|
||||
# order_by
|
||||
assert session.query(For14).order_by(For14.description).all() != []
|
||||
query = session.query(For14).order_by(text_foo) # $ MISSING: getSql=text_foo
|
||||
assert query.all() != []
|
||||
|
||||
# TODO: likewise, it should be possible to target the criterion for:
|
||||
# - `join` https://docs.sqlalchemy.org/en/14/orm/query.html#sqlalchemy.orm.Query.join
|
||||
# - `outerjoin` https://docs.sqlalchemy.org/en/14/orm/query.html#sqlalchemy.orm.Query.outerjoin
|
||||
|
||||
# specifying session later on
|
||||
# see https://docs.sqlalchemy.org/en/14/orm/query.html#sqlalchemy.orm.Query.with_session
|
||||
query = sqlalchemy.orm.Query(For14).filter(For14.description == text_foo) # $ MISSING: getSql=text_foo
|
||||
assert query.with_session(session).all() == []
|
||||
# There are many other possibilities for ending up with SQL injection, including the
|
||||
# following (not an exhaustive list):
|
||||
# - `where` (alias for `filter`)
|
||||
# - `group_by`
|
||||
# - `having`
|
||||
# - `order_by`
|
||||
# - `join`
|
||||
# - `outerjoin`
|
||||
|
||||
# ==============================================================================
|
||||
# v2.0
|
||||
@@ -374,6 +358,8 @@ scalar_result = session.scalar(statement=text_sql) # $ getSql=text_sql
|
||||
assert scalar_result == "FOO"
|
||||
|
||||
# Querying (2.0)
|
||||
# uses a slightly different style than 1.4 -- see note about not modeling
|
||||
# ORM query construction as SQL execution at the 1.4 query tests.
|
||||
|
||||
class For20(Base):
|
||||
__tablename__ = "for20"
|
||||
@@ -397,6 +383,6 @@ statement = sqlalchemy.select(For20)
|
||||
result = session.execute(statement) # $ getSql=statement
|
||||
assert result.scalars().all()[0].id == 20
|
||||
|
||||
statement = sqlalchemy.select(For20).where(For20.description == text_foo) # $ MISSING: getSql=text_foo
|
||||
statement = sqlalchemy.select(For20).where(For20.description == text_foo)
|
||||
result = session.execute(statement) # $ getSql=statement
|
||||
assert result.scalars().all() == []
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import sqlalchemy
|
||||
|
||||
ensure_tainted = ensure_not_tainted = print
|
||||
TAINTED_STRING = "TAINTED_STRING"
|
||||
|
||||
def test_taint():
|
||||
ts = TAINTED_STRING
|
||||
|
||||
ensure_tainted(
|
||||
ts, # $ tainted
|
||||
sqlalchemy.text(ts), # $ tainted
|
||||
sqlalchemy.sql.text(ts),# $ tainted
|
||||
sqlalchemy.sql.expression.text(ts),# $ tainted
|
||||
sqlalchemy.sql.expression.TextClause(ts),# $ tainted
|
||||
)
|
||||
ensure_tainted(ts) # $ tainted
|
||||
|
||||
t1 = sqlalchemy.text(ts)
|
||||
t2 = sqlalchemy.text(text=ts)
|
||||
t3 = sqlalchemy.sql.text(ts)
|
||||
t4 = sqlalchemy.sql.text(text=ts)
|
||||
t5 = sqlalchemy.sql.expression.text(ts)
|
||||
t6 = sqlalchemy.sql.expression.text(text=ts)
|
||||
t7 = sqlalchemy.sql.expression.TextClause(ts)
|
||||
t8 = sqlalchemy.sql.expression.TextClause(text=ts)
|
||||
|
||||
# Since we flag user-input to a TextClause with its' own query, we don't want to
|
||||
# have a taint-step for it as that would lead to us also giving an alert for normal
|
||||
# SQL-injection... and double alerting like this does not seem desireable.
|
||||
ensure_not_tainted(t1, t2, t3, t4, t5, t6, t7, t8)
|
||||
|
||||
for text in [t1, t2, t3, t4, t5, t6, t7, t8]:
|
||||
assert isinstance(text, sqlalchemy.sql.expression.TextClause)
|
||||
|
||||
test_taint()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
edges
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:22:28:22:87 | ControlFlowNode for Attribute() |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:26:50:26:72 | ControlFlowNode for Attribute() |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:36:26:36:33 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:37:31:37:38 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:38:30:38:37 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:39:35:39:42 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:40:41:40:48 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:41:46:41:53 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:42:47:42:54 | ControlFlowNode for username |
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | test.py:43:52:43:59 | ControlFlowNode for username |
|
||||
nodes
|
||||
| test.py:18:15:18:22 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:22:28:22:87 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| test.py:26:50:26:72 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| test.py:36:26:36:33 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:37:31:37:38 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:38:30:38:37 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:39:35:39:42 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:40:41:40:48 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:41:46:41:53 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:42:47:42:54 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
| test.py:43:52:43:59 | ControlFlowNode for username | semmle.label | ControlFlowNode for username |
|
||||
#select
|
||||
| test.py:22:28:22:87 | ControlFlowNode for Attribute() | test.py:18:15:18:22 | ControlFlowNode for username | test.py:22:28:22:87 | ControlFlowNode for Attribute() | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:26:50:26:72 | ControlFlowNode for Attribute() | test.py:18:15:18:22 | ControlFlowNode for username | test.py:26:50:26:72 | ControlFlowNode for Attribute() | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:36:26:36:33 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:36:26:36:33 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:37:31:37:38 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:37:31:37:38 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:38:30:38:37 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:38:30:38:37 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:39:35:39:42 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:39:35:39:42 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:40:41:40:48 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:40:41:40:48 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:41:46:41:53 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:41:46:41:53 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:42:47:42:54 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:42:47:42:54 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
| test.py:43:52:43:59 | ControlFlowNode for username | test.py:18:15:18:22 | ControlFlowNode for username | test.py:43:52:43:59 | ControlFlowNode for username | This SQLAlchemy TextClause depends on $@, which could lead to SQL injection. | test.py:18:15:18:22 | ControlFlowNode for username | a user-provided value |
|
||||
@@ -0,0 +1 @@
|
||||
Security/CWE-089/SQLAlchemyTextClauseInjection.ql
|
||||
@@ -0,0 +1,43 @@
|
||||
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()
|
||||
|
||||
|
||||
# All of these should be flagged by query
|
||||
t1 = sqlalchemy.text(username)
|
||||
t2 = sqlalchemy.text(text=username)
|
||||
t3 = sqlalchemy.sql.text(username)
|
||||
t4 = sqlalchemy.sql.text(text=username)
|
||||
t5 = sqlalchemy.sql.expression.text(username)
|
||||
t6 = sqlalchemy.sql.expression.text(text=username)
|
||||
t7 = sqlalchemy.sql.expression.TextClause(username)
|
||||
t8 = sqlalchemy.sql.expression.TextClause(text=username)
|
||||
Reference in New Issue
Block a user