From 96b7bf46c6ed83d1e64592af665251696177d919 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 21 Nov 2025 20:12:22 +0000 Subject: [PATCH 1/2] PS: Require string concat in the SQL injection query. --- .../powershell/security/SqlInjectionQuery.qll | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/powershell/ql/lib/semmle/code/powershell/security/SqlInjectionQuery.qll b/powershell/ql/lib/semmle/code/powershell/security/SqlInjectionQuery.qll index 6738f0cb59cc..5cf016c62e13 100644 --- a/powershell/ql/lib/semmle/code/powershell/security/SqlInjectionQuery.qll +++ b/powershell/ql/lib/semmle/code/powershell/security/SqlInjectionQuery.qll @@ -12,13 +12,33 @@ import semmle.code.powershell.dataflow.TaintTracking import SqlInjectionCustomizations::SqlInjection import semmle.code.powershell.dataflow.DataFlow -private module Config implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { source instanceof Source } +private module Config implements DataFlow::StateConfigSig { + newtype FlowState = + additional BeforeConcat() or + additional AfterConcat() - predicate isSink(DataFlow::Node sink) { sink instanceof Sink } + predicate isSource(DataFlow::Node source, FlowState state) { + source instanceof Source and state = BeforeConcat() + } + + predicate isSink(DataFlow::Node sink, FlowState state) { + sink instanceof Sink and state = AfterConcat() + } predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer } + predicate isAdditionalFlowStep( + DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2 + ) { + state1 = BeforeConcat() and + state2 = AfterConcat() and + ( + TaintTracking::stringInterpolationTaintStep(node1, node2) + or + TaintTracking::operationTaintStep(node1, node2) + ) + } + predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet cs) { node.(Sink).allowImplicitRead(cs) } @@ -27,4 +47,4 @@ private module Config implements DataFlow::ConfigSig { /** * Taint-tracking for reasoning about SQL-injection vulnerabilities. */ -module SqlInjectionFlow = TaintTracking::Global; +module SqlInjectionFlow = TaintTracking::GlobalWithState; From c1634161a9f920745891337573cdfd8dd3038740 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 21 Nov 2025 20:12:31 +0000 Subject: [PATCH 2/2] PS: Accep test changes. --- .../security/cwe-089/SqlInjection.expected | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/powershell/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/powershell/ql/test/query-tests/security/cwe-089/SqlInjection.expected index 18ec593b3641..0c1da158aca4 100644 --- a/powershell/ql/test/query-tests/security/cwe-089/SqlInjection.expected +++ b/powershell/ql/test/query-tests/security/cwe-089/SqlInjection.expected @@ -1,37 +1,56 @@ edges -| test.ps1:1:1:1:10 | userinput | test.ps1:4:10:4:62 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | provenance | | -| test.ps1:1:1:1:10 | userinput | test.ps1:8:1:8:6 | query | provenance | | -| test.ps1:1:1:1:10 | userinput | test.ps1:17:24:17:76 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | provenance | | -| test.ps1:1:1:1:10 | userinput | test.ps1:28:24:28:76 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | provenance | | -| test.ps1:1:1:1:10 | userinput | test.ps1:78:13:78:59 | SELECT * FROM Customers WHERE id = $userinput | provenance | | -| test.ps1:1:1:1:10 | userinput | test.ps1:128:28:128:37 | userinput | provenance | | +| test.ps1:1:1:1:10 | userinput | test.ps1:4:51:4:60 | userinput | provenance | | | test.ps1:1:14:1:45 | Call to read-host | test.ps1:1:1:1:10 | userinput | provenance | Src:MaD:0 | | test.ps1:4:1:4:6 | query | test.ps1:5:72:5:77 | query | provenance | | | test.ps1:4:10:4:62 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | test.ps1:4:1:4:6 | query | provenance | | +| test.ps1:4:51:4:60 | userinput | test.ps1:4:10:4:62 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | provenance | Config | +| test.ps1:4:51:4:60 | userinput | test.ps1:8:43:8:52 | userinput | provenance | | | test.ps1:8:1:8:6 | query | test.ps1:9:72:9:77 | query | provenance | | +| test.ps1:8:10:8:52 | ...+... | test.ps1:8:1:8:6 | query | provenance | | +| test.ps1:8:43:8:52 | userinput | test.ps1:8:10:8:52 | ...+... | provenance | Config | +| test.ps1:8:43:8:52 | userinput | test.ps1:17:65:17:74 | userinput | provenance | | +| test.ps1:17:65:17:74 | userinput | test.ps1:17:24:17:76 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | provenance | Config | +| test.ps1:17:65:17:74 | userinput | test.ps1:28:65:28:74 | userinput | provenance | | +| test.ps1:28:65:28:74 | userinput | test.ps1:28:24:28:76 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | provenance | Config | +| test.ps1:28:65:28:74 | userinput | test.ps1:78:49:78:58 | userinput | provenance | | | test.ps1:72:1:72:11 | QueryConn2 [element Query] | test.ps1:81:15:81:25 | QueryConn2 | provenance | | | test.ps1:72:15:79:1 | ${...} [element Query] | test.ps1:72:1:72:11 | QueryConn2 [element Query] | provenance | | | test.ps1:78:13:78:59 | SELECT * FROM Customers WHERE id = $userinput | test.ps1:72:15:79:1 | ${...} [element Query] | provenance | | -| test.ps1:121:9:121:56 | unvalidated | test.ps1:125:128:125:142 | $(...) | provenance | | +| test.ps1:78:49:78:58 | userinput | test.ps1:78:13:78:59 | SELECT * FROM Customers WHERE id = $userinput | provenance | Config | +| test.ps1:78:49:78:58 | userinput | test.ps1:111:51:111:60 | userinput | provenance | | +| test.ps1:111:51:111:60 | userinput | test.ps1:128:28:128:37 | userinput | provenance | | +| test.ps1:121:9:121:56 | unvalidated | test.ps1:125:130:125:141 | unvalidated | provenance | | | test.ps1:125:128:125:142 | $(...) | test.ps1:125:92:125:143 | SELECT * FROM Customers where id = $($unvalidated) | provenance | | +| test.ps1:125:128:125:142 | $(...) | test.ps1:125:92:125:143 | SELECT * FROM Customers where id = $($unvalidated) | provenance | Config | +| test.ps1:125:130:125:141 | unvalidated | test.ps1:125:128:125:142 | $(...) | provenance | | +| test.ps1:125:130:125:141 | unvalidated | test.ps1:125:128:125:142 | $(...) | provenance | Config | | test.ps1:128:28:128:37 | userinput | test.ps1:121:9:121:56 | unvalidated | provenance | | nodes | test.ps1:1:1:1:10 | userinput | semmle.label | userinput | | test.ps1:1:14:1:45 | Call to read-host | semmle.label | Call to read-host | | test.ps1:4:1:4:6 | query | semmle.label | query | | test.ps1:4:10:4:62 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | semmle.label | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | +| test.ps1:4:51:4:60 | userinput | semmle.label | userinput | | test.ps1:5:72:5:77 | query | semmle.label | query | | test.ps1:8:1:8:6 | query | semmle.label | query | +| test.ps1:8:10:8:52 | ...+... | semmle.label | ...+... | +| test.ps1:8:43:8:52 | userinput | semmle.label | userinput | | test.ps1:9:72:9:77 | query | semmle.label | query | | test.ps1:17:24:17:76 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | semmle.label | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | +| test.ps1:17:65:17:74 | userinput | semmle.label | userinput | | test.ps1:28:24:28:76 | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | semmle.label | SELECT * FROM MyTable WHERE MyColumn = '$userinput' | +| test.ps1:28:65:28:74 | userinput | semmle.label | userinput | | test.ps1:72:1:72:11 | QueryConn2 [element Query] | semmle.label | QueryConn2 [element Query] | | test.ps1:72:15:79:1 | ${...} [element Query] | semmle.label | ${...} [element Query] | | test.ps1:78:13:78:59 | SELECT * FROM Customers WHERE id = $userinput | semmle.label | SELECT * FROM Customers WHERE id = $userinput | +| test.ps1:78:49:78:58 | userinput | semmle.label | userinput | | test.ps1:81:15:81:25 | QueryConn2 | semmle.label | QueryConn2 | +| test.ps1:111:51:111:60 | userinput | semmle.label | userinput | | test.ps1:121:9:121:56 | unvalidated | semmle.label | unvalidated | | test.ps1:125:92:125:143 | SELECT * FROM Customers where id = $($unvalidated) | semmle.label | SELECT * FROM Customers where id = $($unvalidated) | | test.ps1:125:128:125:142 | $(...) | semmle.label | $(...) | +| test.ps1:125:128:125:142 | $(...) | semmle.label | $(...) | +| test.ps1:125:130:125:141 | unvalidated | semmle.label | unvalidated | | test.ps1:128:28:128:37 | userinput | semmle.label | userinput | subpaths #select