diff --git a/python/CWE-089/SqlInjectionAudit.md b/python/CWE-089/SqlInjectionAudit.md new file mode 100644 index 0000000000..c82350ad65 --- /dev/null +++ b/python/CWE-089/SqlInjectionAudit.md @@ -0,0 +1,19 @@ +# Audit - SQL Injection using format strings + +Dynamically generated SQL queries using format strings can cause SQL injection attacks. The following example shows how to use the `sql` package to execute a query with a format string: + +## Example + +```python +# Format string +query = f"SELECT * FROM users WHERE username = '{username}'" +cursor.execute(query) + +# str.format() +query = "SELECT * FROM users WHERE username = '{}'".format(username) +cursor.execute(query) + +# "%s" % string +query = "SELECT * FROM users WHERE username = %s" % username +cursor.execute(query) +``` diff --git a/python/CWE-089/SqlInjectionAudit.ql b/python/CWE-089/SqlInjectionAudit.ql new file mode 100644 index 0000000000..a18797474d --- /dev/null +++ b/python/CWE-089/SqlInjectionAudit.ql @@ -0,0 +1,41 @@ +/** + * @name SQL query built from user-controlled sources + * @kind path-problem + * @problem.severity warning + * @security-severity 2.5 + * @sub-severity low + * @precision very-low + * @id py/audit/sql-injection + * @tags security + * external/cwe/cwe-089 + * audit + */ + +import python +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import semmle.python.Concepts +import semmle.python.dataflow.new.BarrierGuards +import semmle.python.ApiGraphs +import DataFlow::PathGraph +private import semmle.python.security.dataflow.SqlInjectionCustomizations +// +import github.Utils + +/** + * A taint-tracking configuration for detecting SQL injection vulnerabilities. + */ +class SqlInjectionHeuristic extends TaintTracking::Configuration { + SqlInjectionHeuristic() { this = "SqlInjectionHeuristic" } + + override predicate isSource(DataFlow::Node source) { source instanceof DynamicStrings } + + override predicate isSink(DataFlow::Node sink) { sink instanceof SqlInjection::Sink } + + override predicate isSanitizer(DataFlow::Node node) { node instanceof SqlInjection::Sanitizer } +} + +from SqlInjectionHeuristic config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "This SQL query depends on $@.", source.getNode(), + "a user-provided value" diff --git a/python/github/Utils.qll b/python/github/Utils.qll new file mode 100644 index 0000000000..7fbf9c2d9e --- /dev/null +++ b/python/github/Utils.qll @@ -0,0 +1,36 @@ +import python +private import semmle.python.ApiGraphs +private import semmle.python.Concepts +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.internal.TaintTrackingPrivate + +// List of all the format strings +// - python/ql/lib/semmle/python/dataflow/new/internal/TaintTrackingPrivate.qll +class DynamicStrings extends DataFlow::Node { + DynamicStrings() { + ( + // s = f"WHERE name = '{input}'" + exists(Fstring fmtstr | this.asExpr() = fmtstr) + or + // "SELECT * FROM users WHERE username = '{}'".format(username) + exists(CallNode format, string methods, ControlFlowNode object | + object = format.getFunction().(AttrNode).getObject(methods) + | + methods = "format" and + this.asExpr() = format.getNode() + ) + or + exists(BinaryExpr expr | + ( + // q = "WHERE name = %s" % username + expr.getOp() instanceof Mod or + // q = "WHERE name = " + username + expr.getOp() instanceof Add + ) + and + expr.getLeft().getParent() = this.asExpr() + ) + ) and + this.getScope().inSource() + } +} diff --git a/tests/python-tests/CWE-089/audit/SqlInjectionAudit.expected b/tests/python-tests/CWE-089/audit/SqlInjectionAudit.expected new file mode 100644 index 0000000000..c70e454300 --- /dev/null +++ b/tests/python-tests/CWE-089/audit/SqlInjectionAudit.expected @@ -0,0 +1,20 @@ +edges +| sqli.py:17:9:17:60 | ControlFlowNode for Fstring | sqli.py:18:16:18:20 | ControlFlowNode for query | +| sqli.py:21:9:21:68 | ControlFlowNode for Attribute() | sqli.py:22:16:22:20 | ControlFlowNode for query | +| sqli.py:25:9:25:60 | ControlFlowNode for BinaryExpr | sqli.py:26:16:26:20 | ControlFlowNode for query | +| sqli.py:30:9:30:58 | ControlFlowNode for BinaryExpr | sqli.py:31:16:31:20 | ControlFlowNode for query | +nodes +| sqli.py:17:9:17:60 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring | +| sqli.py:18:16:18:20 | ControlFlowNode for query | semmle.label | ControlFlowNode for query | +| sqli.py:21:9:21:68 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| sqli.py:22:16:22:20 | ControlFlowNode for query | semmle.label | ControlFlowNode for query | +| sqli.py:25:9:25:60 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr | +| sqli.py:26:16:26:20 | ControlFlowNode for query | semmle.label | ControlFlowNode for query | +| sqli.py:30:9:30:58 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr | +| sqli.py:31:16:31:20 | ControlFlowNode for query | semmle.label | ControlFlowNode for query | +subpaths +#select +| sqli.py:18:16:18:20 | ControlFlowNode for query | sqli.py:17:9:17:60 | ControlFlowNode for Fstring | sqli.py:18:16:18:20 | ControlFlowNode for query | This SQL query depends on $@. | sqli.py:17:9:17:60 | ControlFlowNode for Fstring | a user-provided value | +| sqli.py:22:16:22:20 | ControlFlowNode for query | sqli.py:21:9:21:68 | ControlFlowNode for Attribute() | sqli.py:22:16:22:20 | ControlFlowNode for query | This SQL query depends on $@. | sqli.py:21:9:21:68 | ControlFlowNode for Attribute() | a user-provided value | +| sqli.py:26:16:26:20 | ControlFlowNode for query | sqli.py:25:9:25:60 | ControlFlowNode for BinaryExpr | sqli.py:26:16:26:20 | ControlFlowNode for query | This SQL query depends on $@. | sqli.py:25:9:25:60 | ControlFlowNode for BinaryExpr | a user-provided value | +| sqli.py:31:16:31:20 | ControlFlowNode for query | sqli.py:30:9:30:58 | ControlFlowNode for BinaryExpr | sqli.py:31:16:31:20 | ControlFlowNode for query | This SQL query depends on $@. | sqli.py:30:9:30:58 | ControlFlowNode for BinaryExpr | a user-provided value | diff --git a/tests/python-tests/CWE-089/audit/SqlInjectionAudit.qlref b/tests/python-tests/CWE-089/audit/SqlInjectionAudit.qlref new file mode 100644 index 0000000000..8c20f5fcf9 --- /dev/null +++ b/tests/python-tests/CWE-089/audit/SqlInjectionAudit.qlref @@ -0,0 +1 @@ +CWE-089/SqlInjectionAudit.ql \ No newline at end of file diff --git a/tests/python-tests/CWE-089/audit/options b/tests/python-tests/CWE-089/audit/options new file mode 100644 index 0000000000..5613b2aa2e --- /dev/null +++ b/tests/python-tests/CWE-089/audit/options @@ -0,0 +1 @@ +semmle-extractor-options: --max-import-depth=0 \ No newline at end of file diff --git a/tests/python-tests/CWE-089/audit/sqli.py b/tests/python-tests/CWE-089/audit/sqli.py new file mode 100644 index 0000000000..6198bb01f4 --- /dev/null +++ b/tests/python-tests/CWE-089/audit/sqli.py @@ -0,0 +1,31 @@ + +import psycopg2 + +# input +username = input("Username:") + +connection = psycopg2.connect( + user="sysadmin", + password="pynative@#29", + host="127.0.0.1", + port="5432", + database="postgres_db" +) +cursor = connection.cursor() + +# test 1 - Format string +query = f"SELECT * FROM users WHERE username = '{username}'" +cursor.execute(query) + +# test 2 - str.format() +query = "SELECT * FROM users WHERE username = '{}'".format(username) +cursor.execute(query) + +# test 3 - %s +query = "SELECT * FROM users WHERE username = %s" % username +cursor.execute(query) + + +# test 4 - string + string +query = "SELECT * FROM users WHERE username = " + username +cursor.execute(query)