From 18f57e4f024452ac8801b873181c5d6418c86305 Mon Sep 17 00:00:00 2001
From: Evgenii Protsenko <procenkoeg@yandex-team.ru>
Date: Mon, 15 Nov 2021 21:30:47 +0300
Subject: [PATCH 1/3] go: add pgx sqli query

---
 ql/src/experimental/CWE-089/Pgx.qll           | 154 ++++++++++++++++++
 .../experimental/CWE-089/PgxSqlInjection.go   |  14 ++
 .../CWE-089/PgxSqlInjection.qhelp             |  43 +++++
 .../experimental/CWE-089/PgxSqlInjection.ql   |  29 ++++
 .../CWE-089/PgxSqlInjectionGood.go            |  12 ++
 5 files changed, 252 insertions(+)
 create mode 100644 ql/src/experimental/CWE-089/Pgx.qll
 create mode 100644 ql/src/experimental/CWE-089/PgxSqlInjection.go
 create mode 100644 ql/src/experimental/CWE-089/PgxSqlInjection.qhelp
 create mode 100644 ql/src/experimental/CWE-089/PgxSqlInjection.ql
 create mode 100644 ql/src/experimental/CWE-089/PgxSqlInjectionGood.go

diff --git a/ql/src/experimental/CWE-089/Pgx.qll b/ql/src/experimental/CWE-089/Pgx.qll
new file mode 100644
index 000000000..d694e074c
--- /dev/null
+++ b/ql/src/experimental/CWE-089/Pgx.qll
@@ -0,0 +1,154 @@
+import go
+
+module Japroc {
+  /** Gets the package name for Jackc. */
+  string pgx3PackagePath() { result = "github.com/jackc/pgx" }
+
+  string pgxStdlibPackagePath() { result = package(["github.com/jackc/pgx"], "stdlib") }
+
+  string pgxPackagePath() {
+    result = package(["github.com/jackc/pgx"], "") and result != pgx3PackagePath()
+  }
+
+  string pgxAnyPackagePath() { result = package(["github.com/jackc/pgx"], "") }
+
+  string pgxPoolPackagePath() { result = package(["github.com/jackc/pgx"], "pgxpool") }
+
+  string pgconnPackagePath() { result = package(["github.com/jackc/pgconn"], "") }
+
+  predicate batchQueries(string pkg, string tp, string m, int arg) {
+    pkg = pgxAnyPackagePath() and
+    tp = "Batch" and
+    (m = "Queue" and arg = 0)
+  }
+
+  predicate stdlibQueries(string pkg, string tp, string m, int arg) {
+    pkg = pgxStdlibPackagePath() and
+    tp = "Conn" and
+    (
+      m = "Prepare" and arg = 0
+      or
+      m = "PrepareContext" and arg = 1
+      or
+      m = "Exec" and arg = 0
+      or
+      m = "ExecContext" and arg = 1
+      or
+      m = "Query" and arg = 0
+      or
+      m = "QueryContext" and arg = 1
+    )
+  }
+
+  predicate v3Queries(string pkg, string tp, string m, int arg) {
+    pkg = pgx3PackagePath() and
+    tp = ["Tx", "Conn", "ConnPool", "ReplicationConn"] and
+    (
+      m = "Exec" and arg = 0
+      or
+      m = "ExecEx" and arg = 1
+      or
+      m = "Prepare" and arg = 1
+      or
+      m = "PrepareEx" and arg = 2
+      or
+      m = "Query" and arg = 0
+      or
+      m = "QueryEx" and arg = 1
+      or
+      m = "QueryRow" and arg = 0
+      or
+      m = "QueryRowEx" and arg = 1
+    )
+  }
+
+  predicate v4Queries(string pkg, string tp, string m, int arg) {
+    pkg = pgxPackagePath() and
+    tp = ["Conn", "Tx"] and
+    (
+      m = "Prepare" and arg = 2
+      or
+      m = "Exec" and arg = 1
+      or
+      m = "Query" and arg = 1
+      or
+      m = "QueryRow" and arg = 1
+      or
+      m = "QueryFunc" and arg = 1
+    )
+  }
+
+  predicate pgxpoolConnPoolQueries(string pkg, string tp, string m, int arg) {
+    pkg = pgxPoolPackagePath() and
+    tp = ["Conn", "Pool"] and
+    (
+      m = "Exec" and arg = 1
+      or
+      m = "Query" and arg = 1
+      or
+      m = "QueryRow" and arg = 1
+      or
+      m = "QueryFunc" and arg = 1
+    )
+  }
+
+  predicate pgxpoolTxQueries(string pkg, string tp, string m, int arg) {
+    pkg = pgxPoolPackagePath() and
+    tp = ["Tx"] and
+    (
+      m = "Exec" and arg = 1
+      or
+      m = "Query" and arg = 1
+      or
+      m = "QueryRow" and arg = 1
+      or
+      m = "QueryFunc" and arg = 1
+      or
+      m = "Prepare" and arg = 2
+    )
+  }
+
+  predicate pgconnQueries(string pkg, string tp, string m, int arg) {
+    pkg = pgconnPackagePath() and
+    tp = "PgConn" and
+    (
+      m = "Exec" and arg = 1
+      or
+      m = "ExecParams" and arg = 1
+      or
+      m = "CopyTo" and arg = 2
+      or
+      m = "CopyFrom" and arg = 2
+      or
+      m = "Prepare" and arg = 2
+    )
+  }
+
+  predicate pgxQueries(string pkg, string tp, string m, int arg) {
+    pgconnQueries(pkg, tp, m, arg)
+    or
+    pgxpoolTxQueries(pkg, tp, m, arg)
+    or
+    pgxpoolConnPoolQueries(pkg, tp, m, arg)
+    or
+    v3Queries(pkg, tp, m, arg)
+    or
+    v4Queries(pkg, tp, m, arg)
+    or
+    stdlibQueries(pkg, tp, m, arg)
+    or
+    batchQueries(pkg, tp, m, arg)
+  }
+
+  /** A model for sinks of github.com/jackc/pgx. */
+  class QueryString extends DataFlow::Node {
+    // class QueryString extends SQL::QueryString {
+    QueryString() {
+      exists(Method meth, string pkg, string tp, string m, int arg |
+        pgxQueries(pkg, tp, m, arg) and
+        meth.hasQualifiedName(pkg, tp, m) and
+        this = meth.getACall().getArgument(arg)
+      )
+    }
+  }
+}
diff --git a/ql/src/experimental/CWE-089/PgxSqlInjection.go b/ql/src/experimental/CWE-089/PgxSqlInjection.go
new file mode 100644
index 000000000..81ff70ad9
--- /dev/null
+++ b/ql/src/experimental/CWE-089/PgxSqlInjection.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/jackc/pgx/v4"
+)
+
+func handler(conn *pgx.Conn, req *http.Request) {
+	q := fmt.Sprintf("SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE",
+		req.URL.Query()["category"])
+	conn.Query(q)
+}
diff --git a/ql/src/experimental/CWE-089/PgxSqlInjection.qhelp b/ql/src/experimental/CWE-089/PgxSqlInjection.qhelp
new file mode 100644
index 000000000..d78e33837
--- /dev/null
+++ b/ql/src/experimental/CWE-089/PgxSqlInjection.qhelp
@@ -0,0 +1,43 @@
+<!DOCTYPE qhelp PUBLIC
+  "-//Semmle//qhelp//EN"
+  "qhelp.dtd">
+<qhelp>
+
+<overview>
+<p>
+If a database query is built from user-provided data without sufficient sanitization,
+a malicious user may be able to run commands that exfiltrate, tamper with,
+or destroy data stored in the database.
+</p>
+</overview>
+
+<recommendation>
+<p>
+Most database connector libraries offer a way of safely embedding untrusted data into a query by
+means of query parameters or prepared statements. Use these features rather than building queries
+by string concatenation.
+</p>
+</recommendation>
+
+<example>
+<p>
+In the following example, assume the function <code>handler</code> is an HTTP request handler in a
+web application, whose parameter <code>req</code> contains the request object:
+</p>
+<sample src="SqlInjection.go"/>
+<p>
+The handler constructs an SQL query involving user input taken from the request object unsafely
+using <code>fmt.Sprintf</code> to embed a request parameter directly into the query string
+<code>q</code>. The parameter may include quote characters, allowing a malicious user to terminate
+the string literal into which the parameter is embedded and add arbitrary SQL code after it.
+</p>
+<p>
+Instead, the untrusted query parameter should be safely embedded using placeholder parameters:
+</p>
+<sample src="SqlInjectionGood.go" />
+</example>
+
+<references>
+<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL injection</a>.</li>
+</references>
+</qhelp>
diff --git a/ql/src/experimental/CWE-089/PgxSqlInjection.ql b/ql/src/experimental/CWE-089/PgxSqlInjection.ql
new file mode 100644
index 000000000..6c907ec79
--- /dev/null
+++ b/ql/src/experimental/CWE-089/PgxSqlInjection.ql
@@ -0,0 +1,29 @@
+/**
+ * @name Database pgx query built from user-controlled sources
+ * @description Building a database pgx query from user-controlled sources is vulnerable to insertion of
+ *              malicious code by the user.
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 8.8
+ * @precision high
+ * @id go/pgx-sql-injection
+ * @tags security
+ *       external/cwe/cwe-089
+ */
+
+import go
+import Pgx
+import DataFlow::PathGraph
+
+class PgxSqlInjectionConfiguration extends TaintTracking::Configuration {
+  PgxSqlInjectionConfiguration() { this = "PgxSqlInjectionConfiguration" }
+
+  override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
+
+  override predicate isSink(DataFlow::Node sink) { sink instanceof Japroc::QueryString }
+}
+
+from PgxSqlInjectionConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
+where cfg.hasFlowPath(source, sink)
+select sink.getNode(), source, sink, "This query depends on $@.", source.getNode(),
+  "a user-provided value"
diff --git a/ql/src/experimental/CWE-089/PgxSqlInjectionGood.go b/ql/src/experimental/CWE-089/PgxSqlInjectionGood.go
new file mode 100644
index 000000000..ea2dc3ca9
--- /dev/null
+++ b/ql/src/experimental/CWE-089/PgxSqlInjectionGood.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+	"net/http"
+
+	"github.com/jackc/pgx/v4"
+)
+
+func handlerGood(conn *pgx.Conn, req *http.Request) {
+	q := "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE"
+	conn.Query(q, req.URL.Query()["category"])
+}

From 797185cbb6d0d7a0c28507509b792a856422a084 Mon Sep 17 00:00:00 2001
From: Evgenii Protsenko <procenkoeg@yandex-team.ru>
Date: Mon, 15 Nov 2021 21:43:11 +0300
Subject: [PATCH 2/3] Move Pgx.qll to frameworks folder

---
 ql/src/experimental/CWE-089/PgxSqlInjection.ql      | 4 ++--
 ql/src/experimental/{CWE-089 => frameworks}/Pgx.qll | 7 ++++++-
 2 files changed, 8 insertions(+), 3 deletions(-)
 rename ql/src/experimental/{CWE-089 => frameworks}/Pgx.qll (95%)

diff --git a/ql/src/experimental/CWE-089/PgxSqlInjection.ql b/ql/src/experimental/CWE-089/PgxSqlInjection.ql
index 6c907ec79..7e6bf15a9 100644
--- a/ql/src/experimental/CWE-089/PgxSqlInjection.ql
+++ b/ql/src/experimental/CWE-089/PgxSqlInjection.ql
@@ -12,7 +12,7 @@
  */
 
 import go
-import Pgx
+import experimental.frameworks.Pgx
 import DataFlow::PathGraph
 
 class PgxSqlInjectionConfiguration extends TaintTracking::Configuration {
@@ -20,7 +20,7 @@ class PgxSqlInjectionConfiguration extends TaintTracking::Configuration {
 
   override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
 
-  override predicate isSink(DataFlow::Node sink) { sink instanceof Japroc::QueryString }
+  override predicate isSink(DataFlow::Node sink) { sink instanceof Pgx::QueryString }
 }
 
 from PgxSqlInjectionConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
diff --git a/ql/src/experimental/CWE-089/Pgx.qll b/ql/src/experimental/frameworks/Pgx.qll
similarity index 95%
rename from ql/src/experimental/CWE-089/Pgx.qll
rename to ql/src/experimental/frameworks/Pgx.qll
index d694e074c..1be4cf38b 100644
--- a/ql/src/experimental/CWE-089/Pgx.qll
+++ b/ql/src/experimental/frameworks/Pgx.qll
@@ -1,6 +1,11 @@
+/**
+ * Provides predicates and class for working with sql injection sinks
+ * from the `github.com/jackc/pgx` and `github.com/jackc/pgconn` packages.
+ */
+
 import go
 
-module Japroc {
+module Pgx {
   /** Gets the package name for Jackc. */
   string pgx3PackagePath() { result = "github.com/jackc/pgx" }
 

From 47346129f578e0e5e738c87c59b53370c642adb3 Mon Sep 17 00:00:00 2001
From: Evgenii Protsenko <procenkoeg@yandex-team.ru>
Date: Fri, 3 Dec 2021 15:40:54 +0300
Subject: [PATCH 3/3] replace list of 1 elem with a static string

---
 ql/src/experimental/frameworks/Pgx.qll | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/ql/src/experimental/frameworks/Pgx.qll b/ql/src/experimental/frameworks/Pgx.qll
index 1be4cf38b..adcdbacbe 100644
--- a/ql/src/experimental/frameworks/Pgx.qll
+++ b/ql/src/experimental/frameworks/Pgx.qll
@@ -9,17 +9,17 @@ module Pgx {
   /** Gets the package name for Jackc. */
   string pgx3PackagePath() { result = "github.com/jackc/pgx" }
 
-  string pgxStdlibPackagePath() { result = package(["github.com/jackc/pgx"], "stdlib") }
+  string pgxStdlibPackagePath() { result = package("github.com/jackc/pgx", "stdlib") }
 
   string pgxPackagePath() {
-    result = package(["github.com/jackc/pgx"], "") and result != pgx3PackagePath()
+    result = package("github.com/jackc/pgx", "") and result != pgx3PackagePath()
   }
 
-  string pgxAnyPackagePath() { result = package(["github.com/jackc/pgx"], "") }
+  string pgxAnyPackagePath() { result = package("github.com/jackc/pgx", "") }
 
-  string pgxPoolPackagePath() { result = package(["github.com/jackc/pgx"], "pgxpool") }
+  string pgxPoolPackagePath() { result = package("github.com/jackc/pgx", "pgxpool") }
 
-  string pgconnPackagePath() { result = package(["github.com/jackc/pgconn"], "") }
+  string pgconnPackagePath() { result = package("github.com/jackc/pgconn", "") }
 
   predicate batchQueries(string pkg, string tp, string m, int arg) {
     pkg = pgxAnyPackagePath() and