From 32a124c05f8a0025a7502b3163706532b8d26744 Mon Sep 17 00:00:00 2001 From: Darek Slusarczyk Date: Mon, 22 Feb 2021 11:03:24 +0100 Subject: [PATCH 01/10] Fix #80329: Add option to specify LOAD DATA LOCAL white list folder * allow the user to specify a folder where files that can be sent via LOAD DATA LOCAL can exist * add mysqli.local_infile_directory for mysqli (ignored if mysqli.allow_local_infile is enabled) * add PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY for pdo_mysql (ignored if PDO::MYSQL_ATTR_LOCAL_INFILE is enabled) * add related tests * fixes for building with libmysql 8.x * small improvement in existing tests * update php.ini-[development|production] files Closes GH-6448. --- ext/mysqli/mysqli.c | 7 +- ext/mysqli/mysqli_api.c | 5 +- ext/mysqli/mysqli_nonapi.c | 4 + ext/mysqli/mysqli_prop.c | 2 +- ext/mysqli/php_mysqli_structs.h | 2 + ext/mysqli/tests/bug77956.phpt | 5 +- ext/mysqli/tests/foo/bar/bar.data | 3 + ext/mysqli/tests/foo/foo.data | 3 + ext/mysqli/tests/local_infile_tools.inc | 12 ++- ...file_overrides_local_infile_directory.phpt | 75 ++++++++++++++++ ext/mysqli/tests/mysqli_constants.phpt | 4 + .../mysqli_local_infile_default_off.phpt | 4 +- ...local_infile_directory_access_allowed.phpt | 80 +++++++++++++++++ ..._local_infile_directory_access_denied.phpt | 65 ++++++++++++++ ...ocal_infile_directory_vs_open_basedir.phpt | 67 +++++++++++++++ ext/mysqli/tests/mysqli_phpinfo.phpt | 2 +- ext/mysqlnd/mysqlnd_connection.c | 17 ++++ ext/mysqlnd/mysqlnd_enum_n_def.h | 2 + ext/mysqlnd/mysqlnd_loaddata.c | 43 +++++++++- ext/mysqlnd/mysqlnd_structs.h | 2 + ext/pdo_mysql/config.w32 | 5 +- ext/pdo_mysql/mysql_driver.c | 30 +++++++ ext/pdo_mysql/pdo_mysql.c | 3 + ext/pdo_mysql/php_pdo_mysql_int.h | 3 + ext/pdo_mysql/tests/bug70389.phpt | 9 +- ext/pdo_mysql/tests/foo/bar/bar.data | 3 + ext/pdo_mysql/tests/foo/foo.data | 3 + .../tests/pdo_mysql___construct_options.phpt | 4 + .../tests/pdo_mysql_class_constants.phpt | 1 + .../pdo_mysql_local_infile_default_off.phpt | 2 + ..._mysql_local_infile_directory_allowed.phpt | 85 +++++++++++++++++++ ...o_mysql_local_infile_directory_denied.phpt | 76 +++++++++++++++++ ...file_overrides_local_infile_directory.phpt | 85 +++++++++++++++++++ .../tests/skipifinfilenotallowed.inc | 6 ++ php.ini-development | 4 + php.ini-production | 4 + 36 files changed, 710 insertions(+), 17 deletions(-) create mode 100644 ext/mysqli/tests/foo/bar/bar.data create mode 100644 ext/mysqli/tests/foo/foo.data create mode 100644 ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt create mode 100644 ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt create mode 100644 ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt create mode 100644 ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt create mode 100644 ext/pdo_mysql/tests/foo/bar/bar.data create mode 100644 ext/pdo_mysql/tests/foo/foo.data create mode 100644 ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt create mode 100644 ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt create mode 100644 ext/pdo_mysql/tests/skipifinfilenotallowed.inc diff --git a/ext/mysqli/mysqli.c b/ext/mysqli/mysqli.c index d4143e55649a8..5814c55776e82 100644 --- a/ext/mysqli/mysqli.c +++ b/ext/mysqli/mysqli.c @@ -499,6 +499,7 @@ PHP_INI_BEGIN() #endif STD_PHP_INI_BOOLEAN("mysqli.reconnect", "0", PHP_INI_SYSTEM, OnUpdateLong, reconnect, zend_mysqli_globals, mysqli_globals) STD_PHP_INI_BOOLEAN("mysqli.allow_local_infile", "0", PHP_INI_SYSTEM, OnUpdateLong, allow_local_infile, zend_mysqli_globals, mysqli_globals) + STD_PHP_INI_ENTRY("mysqli.local_infile_directory", NULL, PHP_INI_SYSTEM, OnUpdateString, local_infile_directory, zend_mysqli_globals, mysqli_globals) PHP_INI_END() /* }}} */ @@ -523,6 +524,7 @@ static PHP_GINIT_FUNCTION(mysqli) mysqli_globals->report_mode = 0; mysqli_globals->report_ht = 0; mysqli_globals->allow_local_infile = 0; + mysqli_globals->local_infile_directory = NULL; mysqli_globals->rollback_on_cached_plink = FALSE; } /* }}} */ @@ -600,6 +602,9 @@ PHP_MINIT_FUNCTION(mysqli) REGISTER_LONG_CONSTANT("MYSQLI_READ_DEFAULT_FILE", MYSQL_READ_DEFAULT_FILE, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("MYSQLI_OPT_CONNECT_TIMEOUT", MYSQL_OPT_CONNECT_TIMEOUT, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOCAL_INFILE", MYSQL_OPT_LOCAL_INFILE, CONST_CS | CONST_PERSISTENT); +#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) + REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOAD_DATA_LOCAL_DIR", MYSQL_OPT_LOAD_DATA_LOCAL_DIR, CONST_CS | CONST_PERSISTENT); +#endif REGISTER_LONG_CONSTANT("MYSQLI_INIT_COMMAND", MYSQL_INIT_COMMAND, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("MYSQLI_OPT_READ_TIMEOUT", MYSQL_OPT_READ_TIMEOUT, CONST_CS | CONST_PERSISTENT); #ifdef MYSQLI_USE_MYSQLND @@ -1021,7 +1026,7 @@ void php_mysqli_fetch_into_hash_aux(zval *return_value, MYSQL_RES * result, zend MYSQL_ROW row; unsigned int i, num_fields; MYSQL_FIELD *fields; - zend_ulong *field_len; + unsigned long *field_len; if (!(row = mysql_fetch_row(result))) { RETURN_NULL(); diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c index b968f735673da..d7f8533230303 100644 --- a/ext/mysqli/mysqli_api.c +++ b/ext/mysqli/mysqli_api.c @@ -1210,7 +1210,7 @@ PHP_FUNCTION(mysqli_fetch_lengths) #ifdef MYSQLI_USE_MYSQLND const size_t *ret; #else - const zend_ulong *ret; + const unsigned long *ret; #endif if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O", &mysql_result, mysqli_result_class_entry) == FAILURE) { @@ -1673,6 +1673,9 @@ static int mysqli_options_get_option_zval_type(int option) case MYSQL_SET_CHARSET_DIR: #if MYSQL_VERSION_ID > 50605 || defined(MYSQLI_USE_MYSQLND) case MYSQL_SERVER_PUBLIC_KEY: +#endif +#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) + case MYSQL_OPT_LOAD_DATA_LOCAL_DIR: #endif return IS_STRING; diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c index 4870a4319a7bf..198a574f7d589 100644 --- a/ext/mysqli/mysqli_nonapi.c +++ b/ext/mysqli/mysqli_nonapi.c @@ -332,6 +332,10 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b unsigned int allow_local_infile = MyG(allow_local_infile); mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&allow_local_infile); +#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) + mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory)); +#endif + end: if (!mysqli_resource) { mysqli_resource = (MYSQLI_RESOURCE *)ecalloc (1, sizeof(MYSQLI_RESOURCE)); diff --git a/ext/mysqli/mysqli_prop.c b/ext/mysqli/mysqli_prop.c index 9749fe5632556..f1ed103001daa 100644 --- a/ext/mysqli/mysqli_prop.c +++ b/ext/mysqli/mysqli_prop.c @@ -267,7 +267,7 @@ static int result_lengths_read(mysqli_object *obj, zval *retval, bool quiet) #ifdef MYSQLI_USE_MYSQLND const size_t *ret; #else - const zend_ulong *ret; + const unsigned long *ret; #endif uint32_t field_count; diff --git a/ext/mysqli/php_mysqli_structs.h b/ext/mysqli/php_mysqli_structs.h index a39e68b276122..b86b58c9444e0 100644 --- a/ext/mysqli/php_mysqli_structs.h +++ b/ext/mysqli/php_mysqli_structs.h @@ -46,6 +46,7 @@ typedef _Bool my_bool; #include #include #include "mysqli_libmysql.h" + #endif /* MYSQLI_USE_MYSQLND */ @@ -276,6 +277,7 @@ ZEND_BEGIN_MODULE_GLOBALS(mysqli) char *default_pw; zend_long reconnect; zend_long allow_local_infile; + char *local_infile_directory; zend_long strict; zend_long error_no; char *error_msg; diff --git a/ext/mysqli/tests/bug77956.phpt b/ext/mysqli/tests/bug77956.phpt index c76e1021e1586..19df063951cc0 100644 --- a/ext/mysqli/tests/bug77956.phpt +++ b/ext/mysqli/tests/bug77956.phpt @@ -55,6 +55,5 @@ $link->close(); unlink('bug77956.data'); ?> --EXPECTF-- -Warning: mysqli::query(): LOAD DATA LOCAL INFILE forbidden in %s on line %d -[006] [2000] LOAD DATA LOCAL INFILE is forbidden, check mysqli.allow_local_infile -done +[006] [2000] LOAD DATA LOCAL INFILE is forbidden, check related settings like mysqli.allow_local_infile|mysqli.local_infile_directory or PDO::MYSQL_ATTR_LOCAL_INFILE|PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY +done \ No newline at end of file diff --git a/ext/mysqli/tests/foo/bar/bar.data b/ext/mysqli/tests/foo/bar/bar.data new file mode 100644 index 0000000000000..56e5e8cdce4da --- /dev/null +++ b/ext/mysqli/tests/foo/bar/bar.data @@ -0,0 +1,3 @@ +97 +98 +99 diff --git a/ext/mysqli/tests/foo/foo.data b/ext/mysqli/tests/foo/foo.data new file mode 100644 index 0000000000000..01e79c32a8c99 --- /dev/null +++ b/ext/mysqli/tests/foo/foo.data @@ -0,0 +1,3 @@ +1 +2 +3 diff --git a/ext/mysqli/tests/local_infile_tools.inc b/ext/mysqli/tests/local_infile_tools.inc index fef400d0a907f..d45d15e6ac3c2 100644 --- a/ext/mysqli/tests/local_infile_tools.inc +++ b/ext/mysqli/tests/local_infile_tools.inc @@ -6,8 +6,7 @@ } } - function check_local_infile_support($link, $engine, $table_name = 'test') { - + function check_local_infile_allowed_by_server($link) { if (!$res = mysqli_query($link, 'SHOW VARIABLES LIKE "local_infile"')) return "Cannot check if Server variable 'local_infile' is set to 'ON'"; @@ -16,6 +15,15 @@ if ('ON' != $row['Value']) return sprintf("Server variable 'local_infile' seems not set to 'ON', found '%s'", $row['Value']); + return ""; + } + + function check_local_infile_support($link, $engine, $table_name = 'test') { + $res = check_local_infile_allowed_by_server($link); + if ($res) { + return $res; + } + if (!mysqli_query($link, sprintf('DROP TABLE IF EXISTS %s', $table_name))) { return "Failed to drop old test table"; } diff --git a/ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt b/ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt new file mode 100644 index 0000000000000..187701ff8c24b --- /dev/null +++ b/ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt @@ -0,0 +1,75 @@ +--TEST-- +mysqli.allow_local_infile overrides mysqli.local_infile_directory +--SKIPIF-- +errno, $link->error)); + +mysqli_close($link); + +?> +--INI-- +open_basedir={PWD} +mysqli.allow_local_infile=1 +mysqli.local_infile_directory={PWD}/foo/bar +--FILE-- +query("DROP TABLE IF EXISTS test")) { + printf("[002] [%d] %s\n", $link->errno, $link->error); + } + + if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) { + printf("[003] [%d] %s\n", $link->errno, $link->error); + } + + $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data'); + if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) { + printf("[004] [%d] %s\n", $link->errno, $link->error); + } + + if ($res = mysqli_query($link, 'SELECT COUNT(id) AS num FROM test')) { + $row = mysqli_fetch_assoc($res); + mysqli_free_result($res); + + $row_count = $row['num']; + $expected_row_count = 3; + if ($row_count != $expected_row_count) { + printf("[005] %d != %d\n", $row_count, $expected_row_count); + } + } else { + printf("[006] [%d] %s\n", $link->errno, $link->error); + } + + $link->close(); + echo "done"; +?> +--CLEAN-- +query($link, 'DROP TABLE IF EXISTS test')) { + printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +$link->close(); +?> +--EXPECT-- +done diff --git a/ext/mysqli/tests/mysqli_constants.phpt b/ext/mysqli/tests/mysqli_constants.phpt index 6297d56b34033..8538fafa4ff24 100644 --- a/ext/mysqli/tests/mysqli_constants.phpt +++ b/ext/mysqli/tests/mysqli_constants.phpt @@ -202,6 +202,10 @@ mysqli.allow_local_infile=1 $expected_constants["MYSQLI_TYPE_JSON"] = true; } + if ($version > 80210 || $IS_MYSQLND) { + $expected_constants['MYSQLI_OPT_LOAD_DATA_LOCAL_DIR'] = true; + } + $unexpected_constants = array(); foreach ($constants as $group => $consts) { diff --git a/ext/mysqli/tests/mysqli_local_infile_default_off.phpt b/ext/mysqli/tests/mysqli_local_infile_default_off.phpt index c2e8aa2dc80ea..65f40129250e3 100644 --- a/ext/mysqli/tests/mysqli_local_infile_default_off.phpt +++ b/ext/mysqli/tests/mysqli_local_infile_default_off.phpt @@ -16,11 +16,11 @@ echo "server: ", $row['Value'], "\n"; mysqli_free_result($res); mysqli_close($link); -echo "connector: ", ini_get("mysqli.allow_local_infile"), "\n"; +echo 'connector: ', ini_get('mysqli.allow_local_infile'), ' ', var_export(ini_get('mysqli.local_infile_directory')), "\n"; print "done!\n"; ?> --EXPECTF-- server: %s -connector: 0 +connector: 0 '' done! diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt new file mode 100644 index 0000000000000..a9c0c82e990ed --- /dev/null +++ b/ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt @@ -0,0 +1,80 @@ +--TEST-- +mysqli.local_infile_directory vs access allowed +--SKIPIF-- +errno, $link->error)); + +mysqli_close($link); + +?> +--INI-- +open_basedir={PWD} +mysqli.allow_local_infile=0 +mysqli.local_infile_directory={PWD}/foo +--FILE-- +query("DROP TABLE IF EXISTS test")) { + printf("[002] [%d] %s\n", $link->errno, $link->error); + } + + if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) { + printf("[003] [%d] %s\n", $link->errno, $link->error); + } + + $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data'); + if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) { + printf("[004] [%d] %s\n", $link->errno, $link->error); + } + + $filepath = str_replace('\\', '/', __DIR__.'/foo/bar/bar.data'); + if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) { + printf("[005] [%d] %s\n", $link->errno, $link->error); + } + + if ($res = mysqli_query($link, 'SELECT COUNT(id) AS num FROM test')) { + $row = mysqli_fetch_assoc($res); + mysqli_free_result($res); + + $row_count = $row['num']; + $expected_row_count = 6; + if ($row_count != $expected_row_count) { + printf("[006] %d != %d\n", $row_count, $expected_row_count); + } + } else { + printf("[007] [%d] %s\n", $link->errno, $link->error); + } + + $link->close(); + echo "done"; +?> +--CLEAN-- +query($link, 'DROP TABLE IF EXISTS test')) { + printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +$link->close(); +?> +--EXPECT-- +done diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt new file mode 100644 index 0000000000000..14521d00f410c --- /dev/null +++ b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt @@ -0,0 +1,65 @@ +--TEST-- +mysqli.local_infile_directory access denied +--SKIPIF-- +errno, $link->error)); + +mysqli_close($link); + +?> +--INI-- +open_basedir={PWD} +mysqli.allow_local_infile=0 +mysqli.local_infile_directory={PWD}/foo/bar +--FILE-- +query("DROP TABLE IF EXISTS test")) { + printf("[002] [%d] %s\n", $link->errno, $link->error); + } + + if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) { + printf("[003] [%d] %s\n", $link->errno, $link->error); + } + + $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data'); + if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) { + printf("[004] [%d] %s\n", $link->errno, $link->error); + } else { + printf("[005] bug! should not happen - access denied expected\n"); + } + + $link->close(); + echo "done"; +?> +--CLEAN-- +query($link, 'DROP TABLE IF EXISTS test')) { + printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +$link->close(); +?> +--EXPECTF-- +[004] [2036] LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file +done diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt new file mode 100644 index 0000000000000..a7a0b0e5ac1c0 --- /dev/null +++ b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt @@ -0,0 +1,67 @@ +--TEST-- +mysqli.local_infile_directory vs open_basedir +--SKIPIF-- +errno, $link->error)); + +mysqli_close($link); + +?> +--INI-- +open_basedir={PWD} +mysqli.allow_local_infile=0 +mysqli.local_infile_directory={PWD}/../ +--FILE-- +query("DROP TABLE IF EXISTS test")) { + printf("[002] [%d] %s\n", $link->errno, $link->error); + } + + if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) { + printf("[003] [%d] %s\n", $link->errno, $link->error); + } + + $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data'); + if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) { + printf("[004] [%d] %s\n", $link->errno, $link->error); + } else { + printf("[005] bug! should not happen - operation not permitted expected\n"); + } + + echo "done"; +?> +--CLEAN-- +query($link, 'DROP TABLE IF EXISTS test')) { + printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +$link->close(); +?> +--EXPECTF-- +Warning: mysqli::query(): open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s) in %s + +Warning: mysqli::query(%s): Failed to open directory: Operation not permitted in %s +[004] [2036] cannot open local_infile_directory +done diff --git a/ext/mysqli/tests/mysqli_phpinfo.phpt b/ext/mysqli/tests/mysqli_phpinfo.phpt index fd0edd4463ab0..ea78a6bb0c101 100644 --- a/ext/mysqli/tests/mysqli_phpinfo.phpt +++ b/ext/mysqli/tests/mysqli_phpinfo.phpt @@ -46,7 +46,7 @@ require_once('skipifconnectfailure.inc'); if ($IS_MYSQLND) { $expected = array( 'size', - 'mysqli.allow_local_infile', + 'mysqli.allow_local_infile', 'mysqli.local_infile_directory', 'mysqli.allow_persistent', 'mysqli.max_persistent' ); foreach ($expected as $k => $entry) diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c index 168e161d1bd77..8730de04b1f7b 100644 --- a/ext/mysqlnd/mysqlnd_connection.c +++ b/ext/mysqlnd/mysqlnd_connection.c @@ -251,6 +251,10 @@ MYSQLND_METHOD(mysqlnd_conn_data, free_options)(MYSQLND_CONN_DATA * conn) mnd_pefree(conn->options->connect_attr, pers); conn->options->connect_attr = NULL; } + if (conn->options->local_infile_directory) { + mnd_pefree(conn->options->local_infile_directory, pers); + conn->options->local_infile_directory = NULL; + } } /* }}} */ @@ -1648,6 +1652,19 @@ MYSQLND_METHOD(mysqlnd_conn_data, set_client_option)(MYSQLND_CONN_DATA * const c conn->options->flags &= ~CLIENT_LOCAL_FILES; } break; + case MYSQL_OPT_LOAD_DATA_LOCAL_DIR: + { + if (conn->options->local_infile_directory) { + mnd_pefree(conn->options->local_infile_directory, conn->persistent); + } + + if (!value || (*value == '\0')) { + conn->options->local_infile_directory = NULL; + } else { + conn->options->local_infile_directory = mnd_pestrdup(value, conn->persistent); + } + break; + } case MYSQL_INIT_COMMAND: { char ** new_init_commands; diff --git a/ext/mysqlnd/mysqlnd_enum_n_def.h b/ext/mysqlnd/mysqlnd_enum_n_def.h index b65e8523b2a85..67d2faef4e719 100644 --- a/ext/mysqlnd/mysqlnd_enum_n_def.h +++ b/ext/mysqlnd/mysqlnd_enum_n_def.h @@ -129,6 +129,7 @@ #define CR_PARAMS_NOT_BOUND 2031 #define CR_INVALID_PARAMETER_NO 2034 #define CR_INVALID_BUFFER_USE 2035 +#define CR_CANT_OPEN_DIR 2036 #define MYSQLND_EE_FILENOTFOUND 7890 @@ -247,6 +248,7 @@ typedef enum mysqlnd_client_option MYSQL_OPT_NET_BUFFER_LENGTH, MYSQL_OPT_TLS_VERSION, MYSQL_OPT_SSL_MODE, + MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MYSQLND_DEPRECATED_ENUM1 = 200, MYSQLND_OPT_INT_AND_FLOAT_NATIVE = 201, MYSQLND_OPT_NET_CMD_BUFFER_SIZE = 202, diff --git a/ext/mysqlnd/mysqlnd_loaddata.c b/ext/mysqlnd/mysqlnd_loaddata.c index 4cd04338777b9..f2534c98e800d 100644 --- a/ext/mysqlnd/mysqlnd_loaddata.c +++ b/ext/mysqlnd/mysqlnd_loaddata.c @@ -149,12 +149,51 @@ mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn, const char * const filenam MYSQLND_INFILE infile; MYSQLND_PFC * net = conn->protocol_frame_codec; MYSQLND_VIO * vio = conn->vio; + bool is_local_infile_enabled = (conn->options->flags & CLIENT_LOCAL_FILES) == CLIENT_LOCAL_FILES; + const char* local_infile_directory = conn->options->local_infile_directory; + bool is_local_infile_dir_set = local_infile_directory != NULL; + bool prerequisities_ok = TRUE; DBG_ENTER("mysqlnd_handle_local_infile"); - if (!(conn->options->flags & CLIENT_LOCAL_FILES)) { + /* + if local_infile is disabled, and local_infile_dir is not set, then operation is forbidden + */ + if (!is_local_infile_enabled && !is_local_infile_dir_set) { SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, - "LOAD DATA LOCAL INFILE is forbidden, check mysqli.allow_local_infile"); + "LOAD DATA LOCAL INFILE is forbidden, check related settings like " + "mysqli.allow_local_infile|mysqli.local_infile_directory or " + "PDO::MYSQL_ATTR_LOCAL_INFILE|PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY"); + prerequisities_ok = FALSE; + } + + /* + if local_infile_dir is set, then check whether it actually exists, and is accessible + */ + if (is_local_infile_dir_set) { + php_stream *stream = php_stream_opendir(local_infile_directory, REPORT_ERRORS, NULL); + if (stream) { + php_stream_closedir(stream); + } else { + SET_CLIENT_ERROR(conn->error_info, CR_CANT_OPEN_DIR, UNKNOWN_SQLSTATE, "cannot open local_infile_directory"); + prerequisities_ok = FALSE; + } + } + + /* + if local_infile is disabled and local_infile_dir is set, then we have to check whether + filename is located inside its subtree + but only in such a case, because when local_infile is enabled, then local_infile_dir is ignored + */ + if (prerequisities_ok && !is_local_infile_enabled && is_local_infile_dir_set) { + if (php_check_specific_open_basedir(local_infile_directory, filename) == -1) { + SET_CLIENT_ERROR(conn->error_info, CR_CANT_OPEN_DIR, UNKNOWN_SQLSTATE, + "LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file"); + prerequisities_ok = FALSE; + } + } + + if (!prerequisities_ok) { /* write empty packet to server */ ret = net->data->m.send(net, vio, empty_packet, 0, conn->stats, conn->error_info); *is_warning = TRUE; diff --git a/ext/mysqlnd/mysqlnd_structs.h b/ext/mysqlnd/mysqlnd_structs.h index 75d8af9acd280..6ee057fc724fe 100644 --- a/ext/mysqlnd/mysqlnd_structs.h +++ b/ext/mysqlnd/mysqlnd_structs.h @@ -231,6 +231,8 @@ typedef struct st_mysqlnd_session_options unsigned int max_allowed_packet; bool int_and_float_native; + + char *local_infile_directory; } MYSQLND_SESSION_OPTIONS; diff --git a/ext/pdo_mysql/config.w32 b/ext/pdo_mysql/config.w32 index 8b5577273db88..48e47f7871866 100644 --- a/ext/pdo_mysql/config.w32 +++ b/ext/pdo_mysql/config.w32 @@ -10,7 +10,10 @@ if (PHP_PDO_MYSQL != "no") { ADD_EXTENSION_DEP('pdo_mysql', 'pdo'); } else { if (CHECK_LIB("libmysql.lib", "pdo_mysql", PHP_PDO_MYSQL) && - CHECK_HEADER_ADD_INCLUDE("mysql.h", "CFLAGS_PDO_MYSQL", PHP_PHP_BUILD + "\\include\\mysql;" + PHP_PDO_MYSQL)) { + CHECK_HEADER_ADD_INCLUDE("mysql.h", "CFLAGS_PDO_MYSQL", + PHP_PDO_MYSQL + "\\include;" + + PHP_PHP_BUILD + "\\include\\mysql;" + + PHP_PDO_MYSQL)) { EXTENSION("pdo_mysql", "pdo_mysql.c mysql_driver.c mysql_statement.c", null, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); } else { WARNING("pdo_mysql not enabled; libraries and headers not found"); diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 6b42335878da8..1e29157c4848f 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -521,6 +521,24 @@ static int pdo_mysql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_ ZVAL_BOOL(return_value, H->local_infile); break; +#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) + case PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY: + { + const char* local_infile_directory = NULL; +#ifdef PDO_USE_MYSQLND + local_infile_directory = H->server->data->options->local_infile_directory; +#else + mysql_get_option(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, &local_infile_directory); +#endif + if (local_infile_directory) { + ZVAL_STRING(return_value, local_infile_directory); + } else { + ZVAL_NULL(return_value); + } + break; + } +#endif + default: PDO_DBG_RETURN(0); } @@ -682,6 +700,7 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* handle MySQL options */ if (driver_options) { zend_long connect_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30); + zend_string *local_infile_directory = NULL; zend_string *init_cmd = NULL; #ifndef PDO_USE_MYSQLND zend_string *default_file = NULL, *default_group = NULL; @@ -724,6 +743,17 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) #endif } +#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) + local_infile_directory = pdo_attr_strval(driver_options, PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, NULL); + if (local_infile_directory) { + if (mysql_options(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, (const char *)ZSTR_VAL(local_infile_directory))) { + zend_string_release(local_infile_directory); + pdo_mysql_error(dbh); + goto cleanup; + } + zend_string_release(local_infile_directory); + } +#endif #ifdef MYSQL_OPT_RECONNECT /* since 5.0.3, the default for this option is 0 if not specified. * we want the old behaviour diff --git a/ext/pdo_mysql/pdo_mysql.c b/ext/pdo_mysql/pdo_mysql.c index 84828edaa45ae..3dbc9146a236d 100644 --- a/ext/pdo_mysql/pdo_mysql.c +++ b/ext/pdo_mysql/pdo_mysql.c @@ -124,6 +124,9 @@ static PHP_MINIT_FUNCTION(pdo_mysql) #ifdef PDO_USE_MYSQLND REGISTER_PDO_CLASS_CONST_LONG("MYSQL_ATTR_SSL_VERIFY_SERVER_CERT", (zend_long)PDO_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT); #endif +#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) + REGISTER_PDO_CLASS_CONST_LONG("MYSQL_ATTR_LOCAL_INFILE_DIRECTORY", (zend_long)PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY); +#endif #ifdef PDO_USE_MYSQLND mysqlnd_reverse_api_register_api(&pdo_mysql_reverse_api); diff --git a/ext/pdo_mysql/php_pdo_mysql_int.h b/ext/pdo_mysql/php_pdo_mysql_int.h index 75287e7904cb9..dac001eb6cd13 100644 --- a/ext/pdo_mysql/php_pdo_mysql_int.h +++ b/ext/pdo_mysql/php_pdo_mysql_int.h @@ -178,6 +178,9 @@ enum { #ifdef PDO_USE_MYSQLND PDO_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT, #endif +#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) + PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, +#endif }; #endif diff --git a/ext/pdo_mysql/tests/bug70389.phpt b/ext/pdo_mysql/tests/bug70389.phpt index 7815b21255740..1c1bb1ad7f5e5 100644 --- a/ext/pdo_mysql/tests/bug70389.phpt +++ b/ext/pdo_mysql/tests/bug70389.phpt @@ -13,6 +13,7 @@ $flags = [ PDO::MYSQL_ATTR_FOUND_ROWS => true, PDO::MYSQL_ATTR_LOCAL_INFILE => true, PDO::ATTR_PERSISTENT => true, + PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY => __DIR__, ]; $std = new StdClass(); @@ -23,11 +24,13 @@ var_dump($flags); ?> --EXPECTF-- -array(3) { +array(4) { [%d]=> bool(true) - [1001]=> + [%d]=> bool(true) - [12]=> + [%d]=> bool(true) + [%d]=> + string(%d) "%s" } diff --git a/ext/pdo_mysql/tests/foo/bar/bar.data b/ext/pdo_mysql/tests/foo/bar/bar.data new file mode 100644 index 0000000000000..3fa90ba01642b --- /dev/null +++ b/ext/pdo_mysql/tests/foo/bar/bar.data @@ -0,0 +1,3 @@ +97;first +98;second +99;third diff --git a/ext/pdo_mysql/tests/foo/foo.data b/ext/pdo_mysql/tests/foo/foo.data new file mode 100644 index 0000000000000..70d8d301e9d7e --- /dev/null +++ b/ext/pdo_mysql/tests/foo/foo.data @@ -0,0 +1,3 @@ +1;one +2;two +3;three diff --git a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt index efbf3c51c812c..65e24846e1cc6 100644 --- a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt @@ -48,6 +48,7 @@ MySQLPDOTest::skip(); PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => 'PDO::MYSQL_ATTR_USE_BUFFERED_QUERY', PDO::MYSQL_ATTR_LOCAL_INFILE => 'PDO::MYSQL_ATTR_LOCAL_INFILE', + PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY => 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY', PDO::MYSQL_ATTR_DIRECT_QUERY => 'PDO::MYSQL_ATTR_DIRECT_QUERY', PDO::MYSQL_ATTR_INIT_COMMAND => 'PDO::MYSQL_ATTR_INIT_COMMAND', @@ -63,6 +64,7 @@ MySQLPDOTest::skip(); PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => 1, /* TODO getAttribute() does not handle it */ PDO::MYSQL_ATTR_LOCAL_INFILE => false, + PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY => null, /* TODO getAttribute() does not handle it */ PDO::MYSQL_ATTR_DIRECT_QUERY => 1, PDO::MYSQL_ATTR_INIT_COMMAND => '', @@ -156,6 +158,8 @@ MySQLPDOTest::skip(); set_option_and_check(33, PDO::MYSQL_ATTR_DIRECT_QUERY, 1, 'PDO::MYSQL_ATTR_DIRECT_QUERY'); set_option_and_check(34, PDO::MYSQL_ATTR_DIRECT_QUERY, 0, 'PDO::MYSQL_ATTR_DIRECT_QUERY'); + set_option_and_check(35, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, __DIR__, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'); + set_option_and_check(36, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, null, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'); } catch (PDOException $e) { printf("[001] %s, [%s] %s Line: %s\n", $e->getMessage(), diff --git a/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt b/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt index c9877f3ac12dc..f0ef8a0b6bf43 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt @@ -27,6 +27,7 @@ if (!extension_loaded('mysqli') && !extension_loaded('mysqlnd')) { "MYSQL_ATTR_SSL_CIPHER" => true, "MYSQL_ATTR_COMPRESS" => true, "MYSQL_ATTR_MULTI_STATEMENTS" => true, + "MYSQL_ATTR_LOCAL_INFILE_DIRECTORY" => true, ); if (!MySQLPDOTest::isPDOMySQLnd()) { diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt index 810adce7e7b7f..5d290aa98d7ef 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt @@ -17,8 +17,10 @@ $pass = PDO_MYSQL_TEST_PASS; $db = new PDO($dsn, $user, $pass); echo var_export($db->getAttribute(PDO::MYSQL_ATTR_LOCAL_INFILE)), "\n"; +echo var_export($db->getAttribute(PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY)), "\n"; echo "done!\n"; ?> --EXPECT-- false +NULL done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt new file mode 100644 index 0000000000000..9bd33c372537d --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt @@ -0,0 +1,85 @@ +--TEST-- +PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY vs access allowed +--SKIPIF-- + +--FILE-- +exec($sql); + if ($ret !== $exp) { + printf("[%03d] Expecting '%s'/%s got '%s'/%s when running '%s', [%s] %s\n", + $offset, $exp, gettype($exp), $ret, gettype($ret), $sql, + $db->errorCode(), implode(' ', $db->errorInfo())); + return false; + } + } catch (PDOException $e) { + printf("[%03d] '%s' has failed, [%s] %s\n", + $offset, $sql, $db->errorCode(), implode(' ', $db->errorInfo())); + return false; + } + + return true; + } + + require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc'); + putenv('PDOTEST_ATTR='.serialize([ + PDO::MYSQL_ATTR_LOCAL_INFILE=>false, + PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY=>__DIR__."/foo" + ])); + $db = MySQLPDOTest::factory(); + MySQLPDOTest::createTestTable($db, MySQLPDOTest::detect_transactional_mysql_engine($db)); + + try { + exec_and_count(1, $db, 'DROP TABLE IF EXISTS test', 0); + exec_and_count(2, $db, sprintf('CREATE TABLE test(id INT NOT NULL PRIMARY KEY, col1 CHAR(10)) ENGINE=%s', PDO_MYSQL_TEST_ENGINE), 0); + + $filepath = str_replace('\\', '/', __DIR__.'/foo/bar/bar.data'); + + $sql = sprintf("LOAD DATA LOCAL INFILE %s INTO TABLE test FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n'", $db->quote($filepath)); + if (exec_and_count(3, $db, $sql, 3)) { + $stmt = $db->query('SELECT id, col1 FROM test ORDER BY id ASC'); + $expected = array( + array("id" => 97, "col1" => "first"), + array("id" => 98, "col1" => "second"), + array("id" => 99, "col1" => "third"), + ); + $ret = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($expected as $offset => $exp) { + foreach ($exp as $key => $value) { + $actual_value = trim(strval($ret[$offset][$key])); + if ($actual_value != $value) { + printf("Results seem wrong, check manually\n"); + echo "------ EXPECTED OUTPUT ------\n"; + var_dump($expected); + echo "------ ACTUAL OUTPUT ------\n"; + var_dump($ret); + break 2; + } + } + } + } + } catch (PDOException $e) { + printf("[001] %s, [%s] %s\n", + $e->getMessage(), + $db->errorCode(), implode(' ', $db->errorInfo())); + } + + print "done!"; +?> +--CLEAN-- +exec('DROP TABLE IF EXISTS test'); +?> +--EXPECT-- +done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt new file mode 100644 index 0000000000000..6708344ed7321 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt @@ -0,0 +1,76 @@ +--TEST-- +PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY vs access denied +--SKIPIF-- + +--FILE-- +exec($sql); + if ($ret !== $exp) { + printf("[%03d] Expecting '%s'/%s got '%s'/%s when running '%s', [%s] %s\n", + $offset, $exp, gettype($exp), $ret, gettype($ret), $sql, + $db->errorCode(), implode(' ', $db->errorInfo())); + return false; + } + } catch (PDOException $e) { + printf("[%03d] '%s' has failed, [%s] %s\n", + $offset, $sql, $db->errorCode(), implode(' ', $db->errorInfo())); + return false; + } + + return true; + } + + require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc'); + putenv('PDOTEST_ATTR='.serialize([ + PDO::MYSQL_ATTR_LOCAL_INFILE=>false, + PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY=>__DIR__."/foo/bar" + ])); + $db = MySQLPDOTest::factory(); + MySQLPDOTest::createTestTable($db, MySQLPDOTest::detect_transactional_mysql_engine($db)); + + try { + exec_and_count(1, $db, 'DROP TABLE IF EXISTS test', 0); + exec_and_count(2, $db, sprintf('CREATE TABLE test(id INT NOT NULL PRIMARY KEY, col1 CHAR(10)) ENGINE=%s', PDO_MYSQL_TEST_ENGINE), 0); + + $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data'); + + $sql = sprintf("LOAD DATA LOCAL INFILE %s INTO TABLE test FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n'", $db->quote($filepath)); + if (exec_and_count(3, $db, $sql, false)) { + $stmt = $db->query('SELECT id, col1 FROM test ORDER BY id ASC'); + $expected = array(); + $ret = $stmt->fetchAll(PDO::FETCH_ASSOC); + if ($ret != $expected) { + printf("Results seem wrong, check manually\n"); + echo "------ EXPECTED OUTPUT ------\n"; + var_dump($expected); + echo "------ ACTUAL OUTPUT ------\n"; + var_dump($ret); + } + } + } catch (PDOException $e) { + printf("[001] %s, [%s] %s\n", + $e->getMessage(), + $db->errorCode(), implode(' ', $db->errorInfo())); + } + + print "done!"; +?> +--CLEAN-- +exec('DROP TABLE IF EXISTS test'); +?> +--EXPECTF-- +Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2036 LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file in %s on line %d +done! diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt new file mode 100644 index 0000000000000..dc71756655e48 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt @@ -0,0 +1,85 @@ +--TEST-- +PDO::MYSQL_ATTR_LOCAL_INFILE overrides PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY +--SKIPIF-- + +--FILE-- +exec($sql); + if ($ret !== $exp) { + printf("[%03d] Expecting '%s'/%s got '%s'/%s when running '%s', [%s] %s\n", + $offset, $exp, gettype($exp), $ret, gettype($ret), $sql, + $db->errorCode(), implode(' ', $db->errorInfo())); + return false; + } + } catch (PDOException $e) { + printf("[%03d] '%s' has failed, [%s] %s\n", + $offset, $sql, $db->errorCode(), implode(' ', $db->errorInfo())); + return false; + } + + return true; + } + + require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc'); + putenv('PDOTEST_ATTR='.serialize([ + PDO::MYSQL_ATTR_LOCAL_INFILE=>true, + PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY=>__DIR__."/foo/bar" + ])); + $db = MySQLPDOTest::factory(); + MySQLPDOTest::createTestTable($db, MySQLPDOTest::detect_transactional_mysql_engine($db)); + + try { + exec_and_count(1, $db, 'DROP TABLE IF EXISTS test', 0); + exec_and_count(2, $db, sprintf('CREATE TABLE test(id INT NOT NULL PRIMARY KEY, col1 CHAR(10)) ENGINE=%s', PDO_MYSQL_TEST_ENGINE), 0); + + $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data'); + + $sql = sprintf("LOAD DATA LOCAL INFILE %s INTO TABLE test FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n'", $db->quote($filepath)); + if (exec_and_count(3, $db, $sql, 3)) { + $stmt = $db->query('SELECT id, col1 FROM test ORDER BY id ASC'); + $expected = array( + array("id" => 1, "col1" => "one"), + array("id" => 2, "col1" => "two"), + array("id" => 3, "col1" => "three"), + ); + $ret = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($expected as $offset => $exp) { + foreach ($exp as $key => $value) { + $actual_value = trim(strval($ret[$offset][$key])); + if ($actual_value != $value) { + printf("Results seem wrong, check manually\n"); + echo "------ EXPECTED OUTPUT ------\n"; + var_dump($expected); + echo "------ ACTUAL OUTPUT ------\n"; + var_dump($ret); + break 2; + } + } + } + } + } catch (PDOException $e) { + printf("[001] %s, [%s] %s\n", + $e->getMessage(), + $db->errorCode(), implode(' ', $db->errorInfo())); + } + + print "done!"; +?> +--CLEAN-- +exec('DROP TABLE IF EXISTS test'); +?> +--EXPECT-- +done! diff --git a/ext/pdo_mysql/tests/skipifinfilenotallowed.inc b/ext/pdo_mysql/tests/skipifinfilenotallowed.inc new file mode 100644 index 0000000000000..abfea299b75bd --- /dev/null +++ b/ext/pdo_mysql/tests/skipifinfilenotallowed.inc @@ -0,0 +1,6 @@ +query("SHOW VARIABLES LIKE 'local_infile'"); +if (($row = $stmt->fetch(PDO::FETCH_ASSOC)) && ($row['value'] != 'ON')) + die("skip Server variable 'local_infile' seems not set to 'ON', found '". $row['value'] ."'"); +?> diff --git a/php.ini-development b/php.ini-development index 2061266e4c885..a8f538785be7d 100644 --- a/php.ini-development +++ b/php.ini-development @@ -1151,6 +1151,10 @@ mysqli.max_persistent = -1 ; https://php.net/mysqli.allow_local_infile ;mysqli.allow_local_infile = On +; It allows the user to specify a folder where files that can be sent via LOAD DATA +; LOCAL can exist. It is ignored if mysqli.allow_local_infile is enabled. +;mysqli.local_infile_directory = + ; Allow or prevent persistent links. ; https://php.net/mysqli.allow-persistent mysqli.allow_persistent = On diff --git a/php.ini-production b/php.ini-production index 708591bb797a7..2d6b45d25a1df 100644 --- a/php.ini-production +++ b/php.ini-production @@ -1153,6 +1153,10 @@ mysqli.max_persistent = -1 ; https://php.net/mysqli.allow_local_infile ;mysqli.allow_local_infile = On +; It allows the user to specify a folder where files that can be sent via LOAD DATA +; LOCAL can exist. It is ignored if mysqli.allow_local_infile is enabled. +;mysqli.local_infile_directory = + ; Allow or prevent persistent links. ; https://php.net/mysqli.allow-persistent mysqli.allow_persistent = On From 660e2ecbe9a2792dba60cdcd64331dc6f5b67425 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 11:18:53 +0100 Subject: [PATCH 02/10] Use CR_LOAD_DATA_LOCAL_INFILE_REJECTED error code --- .../tests/mysqli_local_infile_directory_access_denied.phpt | 4 ++-- .../mysqli_local_infile_directory_vs_open_basedir.phpt | 2 +- ext/mysqlnd/mysqlnd_enum_n_def.h | 2 +- ext/mysqlnd/mysqlnd_loaddata.c | 6 +++--- .../tests/pdo_mysql_local_infile_directory_denied.phpt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt index 14521d00f410c..d2838f106caaf 100644 --- a/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt +++ b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt @@ -60,6 +60,6 @@ if (!$link->query($link, 'DROP TABLE IF EXISTS test')) { $link->close(); ?> ---EXPECTF-- -[004] [2036] LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file +--EXPECT-- +[004] [2068] LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file done diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt index a7a0b0e5ac1c0..076520cb54a4e 100644 --- a/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt +++ b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt @@ -63,5 +63,5 @@ $link->close(); Warning: mysqli::query(): open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s) in %s Warning: mysqli::query(%s): Failed to open directory: Operation not permitted in %s -[004] [2036] cannot open local_infile_directory +[004] [2068] cannot open local_infile_directory done diff --git a/ext/mysqlnd/mysqlnd_enum_n_def.h b/ext/mysqlnd/mysqlnd_enum_n_def.h index 67d2faef4e719..80be26e6226b1 100644 --- a/ext/mysqlnd/mysqlnd_enum_n_def.h +++ b/ext/mysqlnd/mysqlnd_enum_n_def.h @@ -129,7 +129,7 @@ #define CR_PARAMS_NOT_BOUND 2031 #define CR_INVALID_PARAMETER_NO 2034 #define CR_INVALID_BUFFER_USE 2035 -#define CR_CANT_OPEN_DIR 2036 +#define CR_LOAD_DATA_LOCAL_INFILE_REJECTED 2068 #define MYSQLND_EE_FILENOTFOUND 7890 diff --git a/ext/mysqlnd/mysqlnd_loaddata.c b/ext/mysqlnd/mysqlnd_loaddata.c index f2534c98e800d..c00800c451d97 100644 --- a/ext/mysqlnd/mysqlnd_loaddata.c +++ b/ext/mysqlnd/mysqlnd_loaddata.c @@ -160,7 +160,7 @@ mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn, const char * const filenam if local_infile is disabled, and local_infile_dir is not set, then operation is forbidden */ if (!is_local_infile_enabled && !is_local_infile_dir_set) { - SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE, + SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE, "LOAD DATA LOCAL INFILE is forbidden, check related settings like " "mysqli.allow_local_infile|mysqli.local_infile_directory or " "PDO::MYSQL_ATTR_LOCAL_INFILE|PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY"); @@ -175,7 +175,7 @@ mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn, const char * const filenam if (stream) { php_stream_closedir(stream); } else { - SET_CLIENT_ERROR(conn->error_info, CR_CANT_OPEN_DIR, UNKNOWN_SQLSTATE, "cannot open local_infile_directory"); + SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE, "cannot open local_infile_directory"); prerequisities_ok = FALSE; } } @@ -187,7 +187,7 @@ mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn, const char * const filenam */ if (prerequisities_ok && !is_local_infile_enabled && is_local_infile_dir_set) { if (php_check_specific_open_basedir(local_infile_directory, filename) == -1) { - SET_CLIENT_ERROR(conn->error_info, CR_CANT_OPEN_DIR, UNKNOWN_SQLSTATE, + SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE, "LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file"); prerequisities_ok = FALSE; } diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt index 6708344ed7321..6b2abbc64e0af 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt @@ -72,5 +72,5 @@ $db = MySQLPDOTest::factory(); $db->exec('DROP TABLE IF EXISTS test'); ?> --EXPECTF-- -Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2036 LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file in %s on line %d +Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2068 LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file in %s on line %d done! From 8cb5680793fc921091259c79117b0e324464d30e Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 11:22:28 +0100 Subject: [PATCH 03/10] Adjust test expectations to support libmysqlclient --- .../tests/mysqli_local_infile_directory_access_denied.phpt | 4 ++-- .../tests/pdo_mysql_local_infile_directory_denied.phpt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt index d2838f106caaf..8f50e032208cc 100644 --- a/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt +++ b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt @@ -60,6 +60,6 @@ if (!$link->query($link, 'DROP TABLE IF EXISTS test')) { $link->close(); ?> ---EXPECT-- -[004] [2068] LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file +--EXPECTF-- +[004] [2068] LOAD DATA LOCAL INFILE %s done diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt index 6b2abbc64e0af..352482b124253 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt @@ -72,5 +72,5 @@ $db = MySQLPDOTest::factory(); $db->exec('DROP TABLE IF EXISTS test'); ?> --EXPECTF-- -Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2068 LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file in %s on line %d +Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2068 LOAD DATA LOCAL INFILE %s in %s on line %d done! From 7de3071c612410c63720371322afb1bcbe9dd1a5 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 11:45:26 +0100 Subject: [PATCH 04/10] Add early open_basedir check for local_infile dir If mysqlnd is used, we'll later check open_basedir when opening the file. However, if libmysqlclient is used, then we should check the directory in advance. For consistency of behavior, perform the open_basedir check unconditionally. --- ext/mysqli/mysqli_nonapi.c | 4 +++- .../mysqli_local_infile_directory_vs_open_basedir.phpt | 6 ++---- ext/pdo_mysql/mysql_driver.c | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c index 198a574f7d589..0514fb17443f3 100644 --- a/ext/mysqli/mysqli_nonapi.c +++ b/ext/mysqli/mysqli_nonapi.c @@ -333,7 +333,9 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&allow_local_infile); #if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) - mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory)); + if (MyG(local_infile_directory) && !php_check_open_basedir(MyG(local_infile_directory))) { + mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory)); + } #endif end: diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt index 076520cb54a4e..a48606ad665ae 100644 --- a/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt +++ b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt @@ -60,8 +60,6 @@ if (!$link->query($link, 'DROP TABLE IF EXISTS test')) { $link->close(); ?> --EXPECTF-- -Warning: mysqli::query(): open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s) in %s - -Warning: mysqli::query(%s): Failed to open directory: Operation not permitted in %s -[004] [2068] cannot open local_infile_directory +Warning: mysqli_connect(): open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s) in %s on line %d +[004] [2068] LOAD DATA LOCAL INFILE %s done diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 1e29157c4848f..8ffb8fed6cfa3 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -745,7 +745,7 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) #if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) local_infile_directory = pdo_attr_strval(driver_options, PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, NULL); - if (local_infile_directory) { + if (local_infile_directory && !php_check_open_basedir(ZSTR_VAL(local_infile_directory))) { if (mysql_options(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, (const char *)ZSTR_VAL(local_infile_directory))) { zend_string_release(local_infile_directory); pdo_mysql_error(dbh); From 7f4b7d8cc74a0d0c6621432e96458dcddbae8f8c Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 11:47:48 +0100 Subject: [PATCH 05/10] Allow running local infile tests --- azure/libmysqlclient_job.yml | 2 ++ azure/setup.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/azure/libmysqlclient_job.yml b/azure/libmysqlclient_job.yml index 72be92780fb6d..e61b8de95ff9d 100644 --- a/azure/libmysqlclient_job.yml +++ b/azure/libmysqlclient_job.yml @@ -18,6 +18,8 @@ jobs: set -o sudo service mysql start mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS test" + # Ensure local_infile tests can run. + mysql -uroot -proot -e "SET GLOBAL local_infile = true" displayName: 'Setup MySQL server' # Does not support caching_sha2_auth :( #- template: libmysqlclient_test.yml diff --git a/azure/setup.yml b/azure/setup.yml index 21fccd415b5b4..2ca344228cdba 100644 --- a/azure/setup.yml +++ b/azure/setup.yml @@ -5,6 +5,8 @@ steps: sudo service postgresql start sudo service slapd start mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS test" + # Ensure local_infile tests can run. + mysql -uroot -proot -e "SET GLOBAL local_infile = true" sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" sudo -u postgres psql -c "CREATE DATABASE test;" docker exec sql1 /opt/mssql-tools/bin/sqlcmd -S 127.0.0.1 -U SA -P "" -Q "create login pdo_test with password='password', check_policy=off; create user pdo_test for login pdo_test; grant alter, control to pdo_test;" From ca86df351dac8b2d655b7f2abb74e1bbaa5c556c Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 11:49:01 +0100 Subject: [PATCH 06/10] Temporarily enable libmysqlclient tests --- azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bf67866c7ac59..8d4a9d43b257f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,6 +38,10 @@ jobs: parameters: configurationName: MACOS_DEBUG_NTS configurationParameters: '--enable-debug --disable-zts' + - template: azure/libmysqlclient_job.yml + parameters: + configurationName: LIBMYSQLCLIENT_DEBUG_NTS + configurationParameters: '--enable-debug --disable-zts' - ${{ if eq(variables['Build.Reason'], 'Schedule') }}: - template: azure/job.yml parameters: @@ -108,7 +112,3 @@ jobs: configurationName: DEBUG_NTS_REPEAT configurationParameters: '--enable-debug --disable-zts' runTestsParameters: '--repeat 2' - - template: azure/libmysqlclient_job.yml - parameters: - configurationName: LIBMYSQLCLIENT_DEBUG_NTS - configurationParameters: '--enable-debug --disable-zts' From 49ac655ff113ced36762c0f1d56fb2e792b519ef Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 12:33:01 +0100 Subject: [PATCH 07/10] Avoid unused variable warning --- ext/pdo_mysql/mysql_driver.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 8ffb8fed6cfa3..f23fb0206dbe0 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -700,7 +700,6 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* handle MySQL options */ if (driver_options) { zend_long connect_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30); - zend_string *local_infile_directory = NULL; zend_string *init_cmd = NULL; #ifndef PDO_USE_MYSQLND zend_string *default_file = NULL, *default_group = NULL; @@ -744,7 +743,7 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) } #if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) - local_infile_directory = pdo_attr_strval(driver_options, PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, NULL); + zend_string *local_infile_directory = pdo_attr_strval(driver_options, PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, NULL); if (local_infile_directory && !php_check_open_basedir(ZSTR_VAL(local_infile_directory))) { if (mysql_options(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, (const char *)ZSTR_VAL(local_infile_directory))) { zend_string_release(local_infile_directory); From 04af0e277a5ec8f6861b0071def535ad3514abce Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 12:37:18 +0100 Subject: [PATCH 08/10] Fix off by one in version check MYSQL_OPT_LOAD_DATA_LOCAL_DIR should already be supported in 8.0.21 rather than 8.0.22. --- ext/mysqli/mysqli.c | 2 +- ext/mysqli/mysqli_api.c | 2 +- ext/mysqli/mysqli_nonapi.c | 2 +- ext/pdo_mysql/mysql_driver.c | 4 ++-- ext/pdo_mysql/pdo_mysql.c | 2 +- ext/pdo_mysql/php_pdo_mysql_int.h | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/mysqli/mysqli.c b/ext/mysqli/mysqli.c index 5814c55776e82..bc29c74ce0300 100644 --- a/ext/mysqli/mysqli.c +++ b/ext/mysqli/mysqli.c @@ -602,7 +602,7 @@ PHP_MINIT_FUNCTION(mysqli) REGISTER_LONG_CONSTANT("MYSQLI_READ_DEFAULT_FILE", MYSQL_READ_DEFAULT_FILE, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("MYSQLI_OPT_CONNECT_TIMEOUT", MYSQL_OPT_CONNECT_TIMEOUT, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOCAL_INFILE", MYSQL_OPT_LOCAL_INFILE, CONST_CS | CONST_PERSISTENT); -#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND) REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOAD_DATA_LOCAL_DIR", MYSQL_OPT_LOAD_DATA_LOCAL_DIR, CONST_CS | CONST_PERSISTENT); #endif REGISTER_LONG_CONSTANT("MYSQLI_INIT_COMMAND", MYSQL_INIT_COMMAND, CONST_CS | CONST_PERSISTENT); diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c index d7f8533230303..e1abd07135c68 100644 --- a/ext/mysqli/mysqli_api.c +++ b/ext/mysqli/mysqli_api.c @@ -1674,7 +1674,7 @@ static int mysqli_options_get_option_zval_type(int option) #if MYSQL_VERSION_ID > 50605 || defined(MYSQLI_USE_MYSQLND) case MYSQL_SERVER_PUBLIC_KEY: #endif -#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND) case MYSQL_OPT_LOAD_DATA_LOCAL_DIR: #endif return IS_STRING; diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c index 0514fb17443f3..907b1e3fbc92d 100644 --- a/ext/mysqli/mysqli_nonapi.c +++ b/ext/mysqli/mysqli_nonapi.c @@ -332,7 +332,7 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b unsigned int allow_local_infile = MyG(allow_local_infile); mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&allow_local_infile); -#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND) if (MyG(local_infile_directory) && !php_check_open_basedir(MyG(local_infile_directory))) { mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory)); } diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index f23fb0206dbe0..94fa2411d67a4 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -521,7 +521,7 @@ static int pdo_mysql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_ ZVAL_BOOL(return_value, H->local_infile); break; -#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND) case PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY: { const char* local_infile_directory = NULL; @@ -742,7 +742,7 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) #endif } -#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND) zend_string *local_infile_directory = pdo_attr_strval(driver_options, PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, NULL); if (local_infile_directory && !php_check_open_basedir(ZSTR_VAL(local_infile_directory))) { if (mysql_options(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, (const char *)ZSTR_VAL(local_infile_directory))) { diff --git a/ext/pdo_mysql/pdo_mysql.c b/ext/pdo_mysql/pdo_mysql.c index 3dbc9146a236d..1bfc5ff8747f5 100644 --- a/ext/pdo_mysql/pdo_mysql.c +++ b/ext/pdo_mysql/pdo_mysql.c @@ -124,7 +124,7 @@ static PHP_MINIT_FUNCTION(pdo_mysql) #ifdef PDO_USE_MYSQLND REGISTER_PDO_CLASS_CONST_LONG("MYSQL_ATTR_SSL_VERIFY_SERVER_CERT", (zend_long)PDO_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT); #endif -#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND) REGISTER_PDO_CLASS_CONST_LONG("MYSQL_ATTR_LOCAL_INFILE_DIRECTORY", (zend_long)PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY); #endif diff --git a/ext/pdo_mysql/php_pdo_mysql_int.h b/ext/pdo_mysql/php_pdo_mysql_int.h index dac001eb6cd13..6077ce8245b0b 100644 --- a/ext/pdo_mysql/php_pdo_mysql_int.h +++ b/ext/pdo_mysql/php_pdo_mysql_int.h @@ -178,7 +178,7 @@ enum { #ifdef PDO_USE_MYSQLND PDO_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT, #endif -#if MYSQL_VERSION_ID > 80021 || defined(PDO_USE_MYSQLND) +#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND) PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, #endif }; From fef58dfc6ceac6497cb102a1f54da57a0e47d095 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 22 Feb 2021 15:01:42 +0100 Subject: [PATCH 09/10] Fix test failures --- ext/pdo_mysql/tests/bug70389.phpt | 5 +--- .../tests/pdo_mysql___construct_options.phpt | 7 +++-- .../tests/pdo_mysql_class_constants.phpt | 26 ++++++++++++------- .../pdo_mysql_local_infile_default_off.phpt | 3 +++ ..._mysql_local_infile_directory_allowed.phpt | 6 ++--- ...o_mysql_local_infile_directory_denied.phpt | 6 ++--- ...file_overrides_local_infile_directory.phpt | 6 ++--- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/ext/pdo_mysql/tests/bug70389.phpt b/ext/pdo_mysql/tests/bug70389.phpt index 1c1bb1ad7f5e5..adfd65f5ab1f4 100644 --- a/ext/pdo_mysql/tests/bug70389.phpt +++ b/ext/pdo_mysql/tests/bug70389.phpt @@ -13,7 +13,6 @@ $flags = [ PDO::MYSQL_ATTR_FOUND_ROWS => true, PDO::MYSQL_ATTR_LOCAL_INFILE => true, PDO::ATTR_PERSISTENT => true, - PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY => __DIR__, ]; $std = new StdClass(); @@ -24,13 +23,11 @@ var_dump($flags); ?> --EXPECTF-- -array(4) { +array(3) { [%d]=> bool(true) [%d]=> bool(true) [%d]=> bool(true) - [%d]=> - string(%d) "%s" } diff --git a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt index 65e24846e1cc6..1c4e75272ae8f 100644 --- a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt @@ -158,8 +158,11 @@ MySQLPDOTest::skip(); set_option_and_check(33, PDO::MYSQL_ATTR_DIRECT_QUERY, 1, 'PDO::MYSQL_ATTR_DIRECT_QUERY'); set_option_and_check(34, PDO::MYSQL_ATTR_DIRECT_QUERY, 0, 'PDO::MYSQL_ATTR_DIRECT_QUERY'); - set_option_and_check(35, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, __DIR__, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'); - set_option_and_check(36, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, null, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'); + if (defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) { + set_option_and_check(35, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, null, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'); + // libmysqlclient returns the directory with a trailing slash. + // set_option_and_check(36, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, __DIR__, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'); + } } catch (PDOException $e) { printf("[001] %s, [%s] %s Line: %s\n", $e->getMessage(), diff --git a/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt b/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt index f0ef8a0b6bf43..205e059b54184 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt @@ -13,6 +13,16 @@ if (!extension_loaded('mysqli') && !extension_loaded('mysqlnd')) { true, 'MYSQL_ATTR_LOCAL_INFILE' => true, @@ -27,7 +37,6 @@ if (!extension_loaded('mysqli') && !extension_loaded('mysqlnd')) { "MYSQL_ATTR_SSL_CIPHER" => true, "MYSQL_ATTR_COMPRESS" => true, "MYSQL_ATTR_MULTI_STATEMENTS" => true, - "MYSQL_ATTR_LOCAL_INFILE_DIRECTORY" => true, ); if (!MySQLPDOTest::isPDOMySQLnd()) { @@ -39,15 +48,12 @@ if (!extension_loaded('mysqli') && !extension_loaded('mysqlnd')) { if (extension_loaded('mysqlnd')) { $expected['MYSQL_ATTR_SSL_VERIFY_SERVER_CERT'] = true; $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true; - } else if (extension_loaded('mysqli')) { - if (mysqli_get_client_version() > 50605) { - $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true; - } - } else if (MySQLPDOTest::getClientVersion(MySQLPDOTest::factory()) > 50605) { - /* XXX the MySQL client library version isn't exposed with any - constants, the single possibility is to use the PDO::getAttribute(). - This however will fail with no connection. */ - $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true; + } else if (get_client_version() > 50605) { + $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true; + } + + if (MySQLPDOTest::isPDOMySQLnd() || get_client_version() >= 80021) { + $expected['MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'] = true; } /* diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt index 5d290aa98d7ef..9a12f837fb7ae 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt @@ -5,6 +5,9 @@ ensure default for local infile is off require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc'); require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc'); MySQLPDOTest::skip(); +if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) { + die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support"); +} ?> --FILE-- --FILE-- --FILE-- --FILE-- Date: Mon, 22 Feb 2021 16:41:18 +0100 Subject: [PATCH 10/10] Another test fix --- ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt | 2 -- 1 file changed, 2 deletions(-) diff --git a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt index 1c4e75272ae8f..76db58dff202e 100644 --- a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt @@ -48,7 +48,6 @@ MySQLPDOTest::skip(); PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => 'PDO::MYSQL_ATTR_USE_BUFFERED_QUERY', PDO::MYSQL_ATTR_LOCAL_INFILE => 'PDO::MYSQL_ATTR_LOCAL_INFILE', - PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY => 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY', PDO::MYSQL_ATTR_DIRECT_QUERY => 'PDO::MYSQL_ATTR_DIRECT_QUERY', PDO::MYSQL_ATTR_INIT_COMMAND => 'PDO::MYSQL_ATTR_INIT_COMMAND', @@ -64,7 +63,6 @@ MySQLPDOTest::skip(); PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => 1, /* TODO getAttribute() does not handle it */ PDO::MYSQL_ATTR_LOCAL_INFILE => false, - PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY => null, /* TODO getAttribute() does not handle it */ PDO::MYSQL_ATTR_DIRECT_QUERY => 1, PDO::MYSQL_ATTR_INIT_COMMAND => '',