From 600b214c12f53760165016c16b9137eb405daa3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:24:00 +0000 Subject: [PATCH] Add Java CWE-089 SQL injection audit query with tests and docs Co-authored-by: felickz <1760475+felickz@users.noreply.github.com> --- java/src/audit/CWE-089/SqlInjectionAudit.md | 28 ++++++++++++ java/src/audit/CWE-089/SqlInjectionAudit.ql | 43 +++++++++++++++++++ .../audit/CWE-089/SqlInjectionAudit.expected | 24 +++++++++++ .../audit/CWE-089/SqlInjectionAudit.qlref | 2 + .../audit/CWE-089/SqlInjectionAuditTest.java | 17 ++++++++ 5 files changed, 114 insertions(+) create mode 100644 java/src/audit/CWE-089/SqlInjectionAudit.md create mode 100644 java/src/audit/CWE-089/SqlInjectionAudit.ql create mode 100644 java/test/audit/CWE-089/SqlInjectionAudit.expected create mode 100644 java/test/audit/CWE-089/SqlInjectionAudit.qlref create mode 100644 java/test/audit/CWE-089/SqlInjectionAuditTest.java diff --git a/java/src/audit/CWE-089/SqlInjectionAudit.md b/java/src/audit/CWE-089/SqlInjectionAudit.md new file mode 100644 index 00000000..cd2e1630 --- /dev/null +++ b/java/src/audit/CWE-089/SqlInjectionAudit.md @@ -0,0 +1,28 @@ +# Audit - SQL Injection (any source, partial flow) + +Dynamically generated SQL queries built from unvalidated data can cause SQL injection attacks. +This audit query does not restrict itself to recognized remote/user-controlled sources (like +`RemoteFlowSource`). Instead, it performs a partial (backwards) taint-tracking exploration from +every known SQL injection sink, up to a limited number of steps, so that it can surface data that +flows into a SQL query even when the originating source is not (yet) modeled as user input. This +makes it useful for triaging new or unusual sources of tainted data that reach a SQL injection +sink. + +Because any node is considered a potential source, this query is very low precision and is +intended for manual audit, not for inclusion in a default security suite. + +## Example + +```java +import java.sql.Connection; +import java.sql.Statement; + +public class Example { + void run(Connection connection, String username) throws Exception { + Statement statement = connection.createStatement(); + // Building a query by concatenating an unsanitized value is a SQL injection sink, + // regardless of whether `username` is recognized as user input. + statement.executeQuery("SELECT * FROM users WHERE username = '" + username + "'"); + } +} +``` diff --git a/java/src/audit/CWE-089/SqlInjectionAudit.ql b/java/src/audit/CWE-089/SqlInjectionAudit.ql new file mode 100644 index 00000000..bda0ce0a --- /dev/null +++ b/java/src/audit/CWE-089/SqlInjectionAudit.ql @@ -0,0 +1,43 @@ +/** + * @name SQL query built from user-controlled sources + * @description Building a SQL query from user-controlled sources is vulnerable to insertion of + * malicious SQL code by the user. This audit query reports partial flows (any + * source, not just recognized remote/user input) into a known SQL injection sink, + * which is useful for surfacing sources that are not yet modeled. + * @kind path-problem + * @problem.severity warning + * @security-severity 2.5 + * @sub-severity low + * @precision very-low + * @id githubsecuritylab/audit/sql-injection + * @tags security + * external/cwe/cwe-089 + * audit + */ + +import java +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.security.QueryInjection + +private module SqlInjectionAuditConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { any() } + + predicate isSink(DataFlow::Node sink) { sink instanceof QueryInjectionSink } +} + +private module SqlInjectionAuditFlow = TaintTracking::Global; + +int explorationLimit() { result = 10 } + +private module SqlInjectionAuditPartialFlow = + SqlInjectionAuditFlow::FlowExplorationRev; + +import SqlInjectionAuditPartialFlow::PartialPathGraph + +from + SqlInjectionAuditPartialFlow::PartialPathNode source, + SqlInjectionAuditPartialFlow::PartialPathNode sink +where SqlInjectionAuditPartialFlow::partialFlow(source, sink, _) +select sink.getNode(), source, sink, "This SQL query depends on a $@.", source.getNode(), + "user-provided value" diff --git a/java/test/audit/CWE-089/SqlInjectionAudit.expected b/java/test/audit/CWE-089/SqlInjectionAudit.expected new file mode 100644 index 00000000..38e5ebdd --- /dev/null +++ b/java/test/audit/CWE-089/SqlInjectionAudit.expected @@ -0,0 +1,24 @@ +#select +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:6:37:6:51 | username | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:6:37:6:51 | username | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:20:8:59 | "SELECT * FROM users WHERE username = '" | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:20:8:59 | "SELECT * FROM users WHERE username = '" | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:20:8:59 | "SELECT * FROM users WHERE username = '" [Ext] | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:20:8:59 | "SELECT * FROM users WHERE username = '" | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... [Ext] | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:63:8:70 | username | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:63:8:70 | username | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:63:8:70 | username [Ext] | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:63:8:70 | username | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:74:8:76 | "'" | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:74:8:76 | "'" | user-provided value | +| SqlInjectionAuditTest.java:9:28:9:32 | query | SqlInjectionAuditTest.java:8:74:8:76 | "'" [Ext] | SqlInjectionAuditTest.java:9:28:9:32 | query | This SQL query depends on a $@. | SqlInjectionAuditTest.java:8:74:8:76 | "'" | user-provided value | +| SqlInjectionAuditTest.java:15:28:15:36 | hardcoded | SqlInjectionAuditTest.java:13:24:13:44 | "SELECT * FROM users" | SqlInjectionAuditTest.java:15:28:15:36 | hardcoded | This SQL query depends on a $@. | SqlInjectionAuditTest.java:13:24:13:44 | "SELECT * FROM users" | user-provided value | +edges +| SqlInjectionAuditTest.java:6:37:6:51 | username | SqlInjectionAuditTest.java:8:63:8:70 | username | +| SqlInjectionAuditTest.java:8:20:8:59 | "SELECT * FROM users WHERE username = '" | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | +| SqlInjectionAuditTest.java:8:20:8:59 | "SELECT * FROM users WHERE username = '" [Ext] | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | +| SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | +| SqlInjectionAuditTest.java:8:20:8:70 | ... + ... [Ext] | SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | +| SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | SqlInjectionAuditTest.java:9:28:9:32 | query | +| SqlInjectionAuditTest.java:8:63:8:70 | username | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | +| SqlInjectionAuditTest.java:8:63:8:70 | username [Ext] | SqlInjectionAuditTest.java:8:20:8:70 | ... + ... | +| SqlInjectionAuditTest.java:8:74:8:76 | "'" | SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | +| SqlInjectionAuditTest.java:8:74:8:76 | "'" [Ext] | SqlInjectionAuditTest.java:8:20:8:76 | ... + ... | +| SqlInjectionAuditTest.java:13:24:13:44 | "SELECT * FROM users" | SqlInjectionAuditTest.java:15:28:15:36 | hardcoded | diff --git a/java/test/audit/CWE-089/SqlInjectionAudit.qlref b/java/test/audit/CWE-089/SqlInjectionAudit.qlref new file mode 100644 index 00000000..2d843096 --- /dev/null +++ b/java/test/audit/CWE-089/SqlInjectionAudit.qlref @@ -0,0 +1,2 @@ +query: audit/CWE-089/SqlInjectionAudit.ql +postprocess: TestUtilities/PrettyPrintModels.ql diff --git a/java/test/audit/CWE-089/SqlInjectionAuditTest.java b/java/test/audit/CWE-089/SqlInjectionAuditTest.java new file mode 100644 index 00000000..d612e4e0 --- /dev/null +++ b/java/test/audit/CWE-089/SqlInjectionAuditTest.java @@ -0,0 +1,17 @@ +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public class SqlInjectionAuditTest { + void test1(Connection connection, String username) throws SQLException { + Statement statement = connection.createStatement(); + String query = "SELECT * FROM users WHERE username = '" + username + "'"; + statement.executeQuery(query); + } + + void test2(Connection connection) throws SQLException { + String hardcoded = "SELECT * FROM users"; + Statement statement = connection.createStatement(); + statement.executeQuery(hardcoded); + } +}