diff --git a/storage/connect/CMakeLists.txt b/storage/connect/CMakeLists.txt index d4a0c80a5250c..330b61a6458f5 100644 --- a/storage/connect/CMakeLists.txt +++ b/storage/connect/CMakeLists.txt @@ -29,14 +29,14 @@ SET(CONNECT_SOURCES ha_connect.cc connect.cc user_connect.cc mycat.cc fmdlex.c osutil.c rcmsg.c rcmsg.h array.cpp blkfil.cpp colblk.cpp csort.cpp -filamap.cpp filamdbf.cpp filamfix.cpp filamgz.cpp filamtxt.cpp filter.cpp +filamap.cpp filamdbf.cpp filamfix.cpp filamgz.cpp filamtxt.cpp filechk.cpp filter.cpp json.cpp jsonudf.cpp maputil.cpp myconn.cpp myutil.cpp plgdbutl.cpp plugutil.cpp reldef.cpp tabcol.cpp tabdos.cpp tabext.cpp tabfix.cpp tabfmt.cpp tabjson.cpp table.cpp tabmul.cpp tabmysql.cpp taboccur.cpp tabpivot.cpp tabsys.cpp tabtbl.cpp tabutil.cpp tabvir.cpp tabxcl.cpp valblk.cpp value.cpp xindex.cpp xobject.cpp array.h blkfil.h block.h catalog.h checklvl.h colblk.h connect.h csort.h -engmsg.h filamap.h filamdbf.h filamfix.h filamgz.h filamtxt.h +engmsg.h filamap.h filamdbf.h filamfix.h filamgz.h filamtxt.h filechk.h filter.h global.h ha_connect.h inihandl.h json.h jsonudf.h maputil.h msgid.h mycat.h myconn.h myutil.h os.h osutil.h plgcnx.h plgdbsem.h preparse.h reldef.h resource.h tabcol.h tabdos.h tabext.h tabfix.h tabfmt.h tabjson.h tabmul.h diff --git a/storage/connect/bsonudf.cpp b/storage/connect/bsonudf.cpp index bed40a05249af..a621db1a40cbd 100644 --- a/storage/connect/bsonudf.cpp +++ b/storage/connect/bsonudf.cpp @@ -15,6 +15,7 @@ #include #include "bsonudf.h" +#include "filechk.h" #if defined(UNIX) || defined(UNIV_LINUX) #define _O_RDONLY O_RDONLY @@ -4474,6 +4475,16 @@ char *bson_file(UDF_INIT *initid, UDF_ARGS *args, char *result, PlugSubSet(g->Sarea, g->Sarea_Size); fn = MakePSZ(g, args, 0); + if (!fn) { + PUSH_WARNING("Missing file name"); + *is_null = 1; + return NULL; + } else if (!connect_can_access_file(fn)) { + PUSH_WARNING("Access denied: FILE privilege or secure_file_priv violation"); + *is_null = 1; + return NULL; + } + if (args->arg_count > 1) { int pretty = 3, pty = 3; size_t len; @@ -4625,7 +4636,10 @@ char *bfile_make(UDF_INIT *initid, UDF_ARGS *args, char *result, } // endswitch arg_type if (fn) { - if (!bnx.Serialize(g, jvp, fn, pretty)) + if (!connect_can_access_file(fn)) { + PUSH_WARNING("Access denied: FILE privilege or secure_file_priv violation"); + fn = NULL; + } else if (!bnx.Serialize(g, jvp, fn, pretty)) PUSH_WARNING(g->Message); } else PUSH_WARNING("Missing file name"); @@ -6061,6 +6075,16 @@ char *bbin_file(UDF_INIT *initid, UDF_ARGS *args, char *result, fn = MakePSZ(g, args, 0); + if (!fn) { + PUSH_WARNING("Missing file name"); + *error = 1; + goto fin; + } else if (!connect_can_access_file(fn)) { + PUSH_WARNING("Access denied: FILE privilege or secure_file_priv violation"); + *error = 1; + goto fin; + } + for (unsigned int i = 1; i < args->arg_count; i++) if (args->arg_type[i] == INT_RESULT && *(longlong*)args->args[i] < 4) { pretty = (int) * (longlong*)args->args[i]; diff --git a/storage/connect/filechk.cpp b/storage/connect/filechk.cpp new file mode 100644 index 0000000000000..03e88633e99dd --- /dev/null +++ b/storage/connect/filechk.cpp @@ -0,0 +1,41 @@ +/* Copyright (C) MariaDB Corporation Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA +*/ + +#include +#include +#include +#include "filechk.h" + +/* + @return true if the current user may access path. +*/ +bool connect_can_access_file(const char *path) +{ + char real_path[FN_REFLEN]; + + if (!path) + return false; /* callers must check for NULL before calling */ + + if (check_global_access(current_thd, FILE_ACL, true)) + return false; + + /* MY_SAFE_PATH returns NULL on overflow; fail closed rather than validate a truncated path */ + if (!fn_format(real_path, path, mysql_real_data_home, "", + MY_RELATIVE_PATH | MY_UNPACK_FILENAME | MY_RETURN_REAL_PATH | MY_SAFE_PATH)) + return false; + + return is_secure_file_path(real_path); +} diff --git a/storage/connect/filechk.h b/storage/connect/filechk.h new file mode 100644 index 0000000000000..75ab7eda09ec6 --- /dev/null +++ b/storage/connect/filechk.h @@ -0,0 +1,20 @@ +/* Copyright (C) MariaDB Corporation Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA +*/ + +/* File access checks for CONNECT file UDFs */ +#pragma once +bool connect_can_access_file(const char *path); + diff --git a/storage/connect/jsonudf.cpp b/storage/connect/jsonudf.cpp index 9ce1ff5e48bfd..5a6eacdd01ab0 100644 --- a/storage/connect/jsonudf.cpp +++ b/storage/connect/jsonudf.cpp @@ -14,6 +14,7 @@ #include #include "jsonudf.h" +#include "filechk.h" #if defined(UNIX) || defined(UNIV_LINUX) #define _O_RDONLY O_RDONLY @@ -4555,6 +4556,16 @@ char *json_file(UDF_INIT *initid, UDF_ARGS *args, char *result, PlugSubSet(g->Sarea, g->Sarea_Size); fn = MakePSZ(g, args, 0); + if (!fn) { + PUSH_WARNING("Missing file name"); + *is_null = 1; + return NULL; + } else if (!connect_can_access_file(fn)) { + PUSH_WARNING("Access denied: FILE privilege or secure_file_priv violation"); + *is_null = 1; + return NULL; + } + if (args->arg_count > 1) { int pretty = 3, pty = 3; size_t len; @@ -4708,7 +4719,10 @@ char *jfile_make(UDF_INIT *initid, UDF_ARGS *args, char *result, } // endswitch arg_type if (fn) { - if (!Serialize(g, jvp->GetJson(), fn, pretty)) + if (!connect_can_access_file(fn)) { + PUSH_WARNING("Access denied: FILE privilege or secure_file_priv violation"); + fn = NULL; + } else if (!Serialize(g, jvp->GetJson(), fn, pretty)) PUSH_WARNING(g->Message); } else PUSH_WARNING("Missing file name"); @@ -5760,8 +5774,8 @@ my_bool jbin_file_init(UDF_INIT *initid, UDF_ARGS *args, char *message) if (args->arg_count < 1 || args->arg_count > 4) { strcpy(message, "This function only accepts 1 to 4 arguments"); return true; - } else if (args->arg_type[0] != STRING_RESULT || !args->args[0]) { - strcpy(message, "First argument must be a constant string (file name)"); + } else if (args->arg_type[0] != STRING_RESULT) { + strcpy(message, "First argument must be a string (file name)"); return true; } // endifs @@ -5779,7 +5793,10 @@ my_bool jbin_file_init(UDF_INIT *initid, UDF_ARGS *args, char *message) initid->maybe_null = 1; CalcLen(args, false, reslen, memlen); - fl = GetFileLength(args->args[0]); + if (args->args[0]) + fl = GetFileLength(args->args[0]); + else + fl = 100; // What can be done here? reslen += fl; more += fl * M; //memlen += more; @@ -5804,6 +5821,16 @@ char *jbin_file(UDF_INIT *initid, UDF_ARGS *args, char *result, g->Xchk = NULL; fn = MakePSZ(g, args, 0); + if (!fn) { + PUSH_WARNING("Missing file name"); + *error = 1; + goto fin; + } else if (!connect_can_access_file(fn)) { + PUSH_WARNING("Access denied: FILE privilege or secure_file_priv violation"); + *error = 1; + goto fin; + } + for (unsigned int i = 1; i < args->arg_count; i++) if (args->arg_type[i] == INT_RESULT && *(longlong*)args->args[i] < 4) { pretty = (int) * (longlong*)args->args[i]; diff --git a/storage/connect/mysql-test/connect/r/json_bson_file_priv.result b/storage/connect/mysql-test/connect/r/json_bson_file_priv.result new file mode 100644 index 0000000000000..34cbf8c972b0e --- /dev/null +++ b/storage/connect/mysql-test/connect/r/json_bson_file_priv.result @@ -0,0 +1,156 @@ +# +# Test FILE privilege and secure_file_priv enforcement for JSON/BSON file UDFs +# +# Privileged user: path inside secure_file_priv MYSQL_TMP_DIR +select json_file('MYSQL_TMP_DIR/bib0.json', 0) IS NOT NULL AS ok; +ok +1 +select bson_file('MYSQL_TMP_DIR/biblio.json', 0) IS NOT NULL AS ok; +ok +1 +Warnings: +Warning 1105 File pretty format doesn't match the specified pretty value +select jfile_make('{"test":1}', 'MYSQL_TMP_DIR/out.json', 0) IS NOT NULL AS ok; +ok +1 +select bfile_make('{"test":1}', 'MYSQL_TMP_DIR/out.json', 0) IS NOT NULL AS ok; +ok +1 +select jbin_file('MYSQL_TMP_DIR/bib0.json') IS NOT NULL AS ok; +ok +1 +select bbin_file('MYSQL_TMP_DIR/biblio.json') IS NOT NULL AS ok; +ok +1 +# Privileged user: path outside secure_file_priv +select json_file('DATADIR/bib0.json') IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bson_file('DATADIR/biblio.json') IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select jfile_make('{"test":1}', 'DATADIR/out.json', 0) IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bfile_make('{"test":1}', 'DATADIR/out.json', 0) IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select jbin_file('DATADIR/bib0.json') IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bbin_file('DATADIR/biblio.json') IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +# NULL argument: no "access denied", UDF reports its own error +select json_file(NULL) IS NULL AS ok; +ok +1 +Warnings: +Warning 1105 Missing file name +select bson_file(NULL) IS NULL AS ok; +ok +1 +Warnings: +Warning 1105 Missing file name +select jfile_make('{"test":1}', NULL, 0) IS NULL AS ok; +ok +1 +Warnings: +Warning 1105 Missing file name +select bfile_make('{"test":1}', NULL, 0) IS NULL AS ok; +ok +1 +Warnings: +Warning 1105 Missing file name +select jbin_file(NULL) IS NULL AS ok; +ok +1 +Warnings: +Warning 1105 Missing file name +select bbin_file(NULL) IS NULL AS ok; +ok +1 +Warnings: +Warning 1105 Missing file name +# Privileged user: relative path resolving outside secure_file_priv +select json_file('../../bib0.json') IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bson_file('../../biblio.json') IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +# Overlong filename: MY_SAFE_PATH rejects it, fail closed +select json_file(concat('MYSQL_TMP_DIR/', repeat('x', 512))) IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bson_file(concat('MYSQL_TMP_DIR/', repeat('x', 512))) IS NULL AS denied; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +# Non-constant filename expression (args->args[0] is NULL at init time) +set @fname = 'MYSQL_TMP_DIR/bib0.json'; +select jbin_file(@fname) IS NOT NULL AS ok; +ok +1 +set @fname = 'MYSQL_TMP_DIR/biblio.json'; +select bbin_file(@fname) IS NOT NULL AS ok; +ok +1 +# User without FILE privilege +create user unprivileged@localhost; +grant select ON *.* TO unprivileged@localhost; +connect unprivileged,localhost,unprivileged,,; +connection unprivileged; +# inside secure_file_priv: denied without FILE privilege +select json_file('MYSQL_TMP_DIR/bib0.json') IS NULL AS denied;; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bson_file('MYSQL_TMP_DIR/biblio.json') IS NULL AS denied;; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select jfile_make('{"test":1}', 'MYSQL_TMP_DIR/out.json', 0) IS NULL AS denied;; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bfile_make('{"test":1}', 'MYSQL_TMP_DIR/out.json', 0) IS NULL AS denied;; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select jbin_file('MYSQL_TMP_DIR/bib0.json') IS NULL AS denied;; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +select bbin_file('MYSQL_TMP_DIR/biblio.json') IS NULL AS denied;; +denied +1 +Warnings: +Warning 1105 Access denied: FILE privilege or secure_file_priv violation +disconnect unprivileged; +connection default; +drop user unprivileged@localhost; diff --git a/storage/connect/mysql-test/connect/t/json_bson_file_priv.opt b/storage/connect/mysql-test/connect/t/json_bson_file_priv.opt new file mode 100644 index 0000000000000..e9a43a5584d8f --- /dev/null +++ b/storage/connect/mysql-test/connect/t/json_bson_file_priv.opt @@ -0,0 +1 @@ +--secure_file_priv=$MYSQL_TMP_DIR diff --git a/storage/connect/mysql-test/connect/t/json_bson_file_priv.test b/storage/connect/mysql-test/connect/t/json_bson_file_priv.test new file mode 100644 index 0000000000000..04c91184a943b --- /dev/null +++ b/storage/connect/mysql-test/connect/t/json_bson_file_priv.test @@ -0,0 +1,100 @@ +--source include/not_embedded.inc +--source json_udf.inc +--source bson_udf.inc + +let $MYSQLD_DATADIR = `select @@datadir`; + +--copy_file $MTR_SUITE_DIR/std_data/bib0.json $MYSQL_TMP_DIR/bib0.json +--copy_file $MTR_SUITE_DIR/std_data/biblio.json $MYSQL_TMP_DIR/biblio.json + +--echo # +--echo # Test FILE privilege and secure_file_priv enforcement for JSON/BSON file UDFs +--echo # + +--echo # Privileged user: path inside secure_file_priv MYSQL_TMP_DIR +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select json_file('$MYSQL_TMP_DIR/bib0.json', 0) IS NOT NULL AS ok +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bson_file('$MYSQL_TMP_DIR/biblio.json', 0) IS NOT NULL AS ok +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select jfile_make('{"test":1}', '$MYSQL_TMP_DIR/out.json', 0) IS NOT NULL AS ok +--remove_file $MYSQL_TMP_DIR/out.json +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bfile_make('{"test":1}', '$MYSQL_TMP_DIR/out.json', 0) IS NOT NULL AS ok +--remove_file $MYSQL_TMP_DIR/out.json +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select jbin_file('$MYSQL_TMP_DIR/bib0.json') IS NOT NULL AS ok +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bbin_file('$MYSQL_TMP_DIR/biblio.json') IS NOT NULL AS ok + +--echo # Privileged user: path outside secure_file_priv +--replace_result $MYSQLD_DATADIR DATADIR +--eval select json_file('$MYSQLD_DATADIR/bib0.json') IS NULL AS denied +--replace_result $MYSQLD_DATADIR DATADIR +--eval select bson_file('$MYSQLD_DATADIR/biblio.json') IS NULL AS denied +--replace_result $MYSQLD_DATADIR DATADIR +--eval select jfile_make('{"test":1}', '$MYSQLD_DATADIR/out.json', 0) IS NULL AS denied +--replace_result $MYSQLD_DATADIR DATADIR +--eval select bfile_make('{"test":1}', '$MYSQLD_DATADIR/out.json', 0) IS NULL AS denied +--replace_result $MYSQLD_DATADIR DATADIR +--eval select jbin_file('$MYSQLD_DATADIR/bib0.json') IS NULL AS denied +--replace_result $MYSQLD_DATADIR DATADIR +--eval select bbin_file('$MYSQLD_DATADIR/biblio.json') IS NULL AS denied + +--echo # NULL argument: no "access denied", UDF reports its own error +select json_file(NULL) IS NULL AS ok; +select bson_file(NULL) IS NULL AS ok; +select jfile_make('{"test":1}', NULL, 0) IS NULL AS ok; +select bfile_make('{"test":1}', NULL, 0) IS NULL AS ok; +select jbin_file(NULL) IS NULL AS ok; +select bbin_file(NULL) IS NULL AS ok; + +--echo # Privileged user: relative path resolving outside secure_file_priv +select json_file('../../bib0.json') IS NULL AS denied; +select bson_file('../../biblio.json') IS NULL AS denied; + +--echo # Overlong filename: MY_SAFE_PATH rejects it, fail closed +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select json_file(concat('$MYSQL_TMP_DIR/', repeat('x', 512))) IS NULL AS denied +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bson_file(concat('$MYSQL_TMP_DIR/', repeat('x', 512))) IS NULL AS denied + +--echo # Non-constant filename expression (args->args[0] is NULL at init time) +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval set @fname = '$MYSQL_TMP_DIR/bib0.json' +select jbin_file(@fname) IS NOT NULL AS ok; +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval set @fname = '$MYSQL_TMP_DIR/biblio.json' +select bbin_file(@fname) IS NOT NULL AS ok; + +--echo # User without FILE privilege +create user unprivileged@localhost; +grant select ON *.* TO unprivileged@localhost; + +--connect(unprivileged,localhost,unprivileged,,) +--connection unprivileged + +--echo # inside secure_file_priv: denied without FILE privilege +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select json_file('$MYSQL_TMP_DIR/bib0.json') IS NULL AS denied; +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bson_file('$MYSQL_TMP_DIR/biblio.json') IS NULL AS denied; +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select jfile_make('{"test":1}', '$MYSQL_TMP_DIR/out.json', 0) IS NULL AS denied; +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bfile_make('{"test":1}', '$MYSQL_TMP_DIR/out.json', 0) IS NULL AS denied; +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select jbin_file('$MYSQL_TMP_DIR/bib0.json') IS NULL AS denied; +--replace_result $MYSQL_TMP_DIR MYSQL_TMP_DIR +--eval select bbin_file('$MYSQL_TMP_DIR/biblio.json') IS NULL AS denied; + +--disconnect unprivileged +--connection default + +--remove_file $MYSQL_TMP_DIR/bib0.json +--remove_file $MYSQL_TMP_DIR/biblio.json + +drop user unprivileged@localhost; + +--source json_udf2.inc +--source bson_udf2.inc