From 32adcb1dbfdf9df73ccdd20a77d1aa69132539ef Mon Sep 17 00:00:00 2001
From: Cal Herries <39073188+calherries@users.noreply.github.com>
Date: Fri, 24 Feb 2023 17:40:19 +0200
Subject: [PATCH] Whitelist H2 commands for actions (#28583)

* Decouple checking ddl from classifying h2 stmts

- should enable followup for easily blocking more kinds of queries
- check all statements to make sure they aren't "ddl".

* fix classify-query

* linter fixes + get-field refactor

* return the CommandInterface values as ints

Reach into the CommandList when needed

* docstring wording

* catch invalid queries -- they can't be classified

* Remove action subtypes from inlined-models

* Add ddl check for `execute-write-query!`

* Remove ACCESS_MODE_DATA

* Check queries are single select statements

* Add test for sample database privileges

* Fix single-select check

* Add single-select test

* Rename and add more tests for checking read only commands

* commands -> statements

* Fix check-disallow-ddl-commands

* new line

* Add more read-only statements to the tests

* Update error text

* Use are

* Add integration test for executing actions with disallowed commands

* Add test before inserting row

* Run GRANT ALL ON SCHEMA "PUBLIC" TO GUEST

* Restore classify-query

* whitespace

* Whitelist command types for actions

* Add comment

* Rename

* Remove dupes

* Add truncate test

* Add DDL commands

* Rename to query-classification

* Update error message

* Fix test

---------

Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
---
 src/metabase/driver/h2.clj       | 32 +++++++++++++++++---------------
 test/metabase/driver/h2_test.clj | 23 +++++++++++++++--------
 2 files changed, 32 insertions(+), 23 deletions(-)

diff --git a/src/metabase/driver/h2.clj b/src/metabase/driver/h2.clj
index 32438e9cafc..a3fbadb8083 100644
--- a/src/metabase/driver/h2.clj
+++ b/src/metabase/driver/h2.clj
@@ -164,25 +164,27 @@
       (catch org.h2.message.DbException _
         {:command-types [] :remaining-sql nil}))))
 
-(defn- cmd-type-ddl? [cmd-type]
-  ;; Command types are organized with all DDL commands listed first, so all ddl commands are before ALTER_SEQUENCE.
-  ;; see https://github.com/h2database/h2database/blob/master/h2/src/main/org/h2/command/CommandInterface.java#L297
-  (< cmd-type CommandInterface/ALTER_SEQUENCE))
-
-(defn- contains-ddl? [{:keys [command-types remaining-sql]}]
+(defn- every-command-allowed-for-actions? [{:keys [command-types remaining-sql]}]
   (let [cmd-type-nums command-types]
     (boolean
-     (or (some cmd-type-ddl? cmd-type-nums)
-         (some? remaining-sql)))))
-
-;; TODO: black-list RUNSCRIPT, and a bunch more -- but they're not technically ddl. Should be simple to build off of [[classify-query]].
-;; e.g.: similar to contains-ddl? but instead of cmd-type-ddl? use: #(#{CommandInterface/RUNSCRIPT} %)
+     ;; Command types are organized with all DDL commands listed first, so all ddl commands are before ALTER_SEQUENCE.
+     ;; see https://github.com/h2database/h2database/blob/master/h2/src/main/org/h2/command/CommandInterface.java#L297
+     (and (every? #{CommandInterface/INSERT
+                    CommandInterface/MERGE
+                    CommandInterface/TRUNCATE_TABLE
+                    CommandInterface/UPDATE
+                    CommandInterface/DELETE
+                    ;; Read-only commands might not make sense for actions, but they are allowed
+                    CommandInterface/SELECT ; includes SHOW, TABLE, VALUES
+                    CommandInterface/EXPLAIN
+                    CommandInterface/CALL} cmd-type-nums)
+          (nil? remaining-sql)))))
 
-(defn- check-disallow-ddl-commands [{:keys [database] {:keys [query]} :native}]
+(defn- check-action-commands-allowed [{:keys [database] {:keys [query]} :native}]
   (when query
     (when-let [query-classification (classify-query database query)]
-      (when (contains-ddl? query-classification)
-        (throw (ex-info "IllegalArgument: DDL commands are not allowed to be used with h2."
+      (when-not (every-command-allowed-for-actions? query-classification)
+        (throw (ex-info "DDL commands are not allowed to be used with H2."
                         {:classification query-classification}))))))
 
 (defn- read-only-statements? [{:keys [command-types remaining-sql]}]
@@ -209,7 +211,7 @@
 (defmethod driver/execute-write-query! :h2
   [driver query]
   (check-native-query-not-using-default-user query)
-  (check-disallow-ddl-commands query)
+  (check-action-commands-allowed query)
   ((get-method driver/execute-write-query! :sql-jdbc) driver query))
 
 (defmethod sql.qp/add-interval-honeysql-form :h2
diff --git a/test/metabase/driver/h2_test.clj b/test/metabase/driver/h2_test.clj
index 9820914593e..cb87b9a1e33 100644
--- a/test/metabase/driver/h2_test.clj
+++ b/test/metabase/driver/h2_test.clj
@@ -168,9 +168,9 @@
                  (some-> (qp/compile query) :query pretty-sql))))))))
 
 
-(deftest classify-ddl-test
+(deftest check-action-commands-test
   (mt/test-driver :h2
-    (are [query] (= false (#'h2/contains-ddl? (#'h2/classify-query (u/the-id (mt/db)) query)))
+    (are [query] (= true (#'h2/every-command-allowed-for-actions? (#'h2/classify-query (u/the-id (mt/db)) query)))
       "select 1"
       "update venues set name = 'bill'"
       "delete venues"
@@ -179,9 +179,16 @@
        delete venues;"
       "update venues set name = 'stomp';"
       "select * from venues; update venues set name = 'stomp';"
-      "update venues set name = 'stomp'; select * from venues;")
+      "update venues set name = 'stomp'; select * from venues;"
+      "truncate table venues"
+      "insert into venues values (1, 'Chicken Chow')"
+      "merge into venues key(1) values (1, 'Chicken Chow')"
+      "merge into venues using (select 1 as id) as source on (venues.id = source.id) when matched then update set name = 'Chicken Chow';")
 
-    (are [query] (= true (#'h2/contains-ddl? (#'h2/classify-query (u/the-id (mt/db)) query)))
+    (are [query] (= false (#'h2/every-command-allowed-for-actions? (#'h2/classify-query (u/the-id (mt/db)) query)))
+      "create table venues (id int, name varchar(255))"
+      "alter table venues add column address varchar(255)"
+      "drop table venues"
       "select * from venues; update venues set name = 'stomp';
        CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return \"y4tacker\";}';
        EXEC ('open -a Calculator.app')"
@@ -189,9 +196,9 @@
        CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return \"y4tacker\";}';"
       "CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return \"y4tacker\";}';")
 
-    (is (= nil (#'h2/check-disallow-ddl-commands {:database (u/the-id (mt/db)) :native {:query nil}})))
+    (is (= nil (#'h2/check-action-commands-allowed {:database (u/the-id (mt/db)) :native {:query nil}})))
 
-    (is (= nil (#'h2/check-disallow-ddl-commands
+    (is (= nil (#'h2/check-action-commands-allowed
                 {:database (u/the-id (mt/db))
                  :engine :h2
                  :native {:query (str/join "; "
@@ -203,7 +210,7 @@
                                                    "SELECT * FROM INFORMATION_SCHEMA.Users;"])]
       (is (thrown? clojure.lang.ExceptionInfo
                    #"DDL commands are not allowed to be used with h2."
-                   (#'h2/check-disallow-ddl-commands
+                   (#'h2/check-action-commands-allowed
                     {:database (u/the-id (mt/db))
                      :engine :h2
                      :native {:query trigger-creation-attempt}}))))))
@@ -251,7 +258,7 @@
                                                  :dataset_query {:database (mt/id)
                                                                  :type     "native"
                                                                  :native   {:query sql}}}]
-            (is (=? {:message "Error executing Action: IllegalArgument: DDL commands are not allowed to be used with h2."}
+            (is (=? {:message "Error executing Action: DDL commands are not allowed to be used with H2."}
                     (mt/user-http-request :crowberto
                                           :post 500
                                           (format "action/%s/execute" action-id)))))))
-- 
GitLab