summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDarek Slusarczyk <dariusz.slusarczyk@oracle.com>2021-02-22 11:03:24 +0100
committerNikita Popov <nikita.ppv@gmail.com>2021-02-23 09:30:46 +0100
commitda011a312a6c6cd7ff12fe1aa0de1e33fba2f167 (patch)
treec3e37dadfa5ea7aec39ca3695779fe30c7bd96f6
parent7f8ea83ef438fbcfa1cbc636d701491d4e773245 (diff)
downloadphp-git-da011a312a6c6cd7ff12fe1aa0de1e33fba2f167.tar.gz
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. Co-authored-by: Nikita Popov <nikic@php.net>
-rw-r--r--NEWS2
-rw-r--r--UPGRADING12
-rw-r--r--azure/libmysqlclient_job.yml2
-rw-r--r--azure/setup.yml2
-rw-r--r--ext/mysqli/mysqli.c7
-rw-r--r--ext/mysqli/mysqli_api.c5
-rw-r--r--ext/mysqli/mysqli_nonapi.c6
-rw-r--r--ext/mysqli/mysqli_prop.c2
-rw-r--r--ext/mysqli/php_mysqli_structs.h2
-rw-r--r--ext/mysqli/tests/bug77956.phpt5
-rw-r--r--ext/mysqli/tests/foo/bar/bar.data3
-rw-r--r--ext/mysqli/tests/foo/foo.data3
-rw-r--r--ext/mysqli/tests/local_infile_tools.inc12
-rw-r--r--ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt75
-rw-r--r--ext/mysqli/tests/mysqli_constants.phpt4
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_default_off.phpt4
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt80
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt65
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt65
-rw-r--r--ext/mysqli/tests/mysqli_phpinfo.phpt2
-rw-r--r--ext/mysqlnd/mysqlnd_connection.c17
-rw-r--r--ext/mysqlnd/mysqlnd_enum_n_def.h2
-rw-r--r--ext/mysqlnd/mysqlnd_loaddata.c45
-rw-r--r--ext/mysqlnd/mysqlnd_structs.h2
-rw-r--r--ext/pdo_mysql/config.w325
-rw-r--r--ext/pdo_mysql/mysql_driver.c29
-rw-r--r--ext/pdo_mysql/pdo_mysql.c3
-rw-r--r--ext/pdo_mysql/php_pdo_mysql_int.h3
-rw-r--r--ext/pdo_mysql/tests/bug70389.phpt4
-rw-r--r--ext/pdo_mysql/tests/foo/bar/bar.data3
-rw-r--r--ext/pdo_mysql/tests/foo/foo.data3
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt5
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt25
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt5
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt85
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt76
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt85
-rw-r--r--ext/pdo_mysql/tests/skipifinfilenotallowed.inc6
-rw-r--r--php.ini-development4
-rw-r--r--php.ini-production4
40 files changed, 743 insertions, 26 deletions
diff --git a/NEWS b/NEWS
index 4020fd7bb8..02e3c77cf9 100644
--- a/NEWS
+++ b/NEWS
@@ -24,6 +24,8 @@ PHP NEWS
. Fixed bug #70372 (Emulate mysqli_fetch_all() for libmysqlclient). (Nikita)
. Fixed bug #80330 (Replace language in APIs and source code/docs).
(Darek Ślusarczyk)
+ . Fixed bug #80329 (Add option to specify LOAD DATA LOCAL white list folder
+ (including libmysql)). (Darek Ślusarczyk)
- Opcache:
. Added inheritance cache. (Dmitry)
diff --git a/UPGRADING b/UPGRADING
index eb01058a5a..f5c00596e4 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -176,6 +176,18 @@ PHP 8.1 UPGRADE NOTES
Note, that the quality of the custom secret is crucial for the quality of the resulting hash. It is
highly recommended for the secret to use the best possible entropy.
+- MySQLi:
+ . The mysqli.local_infile_directory ini setting has been added, which can be
+ used to specify a directory from which files are allowed to be loaded. It
+ is only meaningful if mysqli.allow_local_infile is not enabled, as all
+ directories are allowed in that case.
+
+- PDO MySQL:
+ . The PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY attribute has been added, which
+ can be used to specify a directory from which files are allowed to be
+ loaded. It is only meaningful if PDO::MYSQL_ATTR_LOCAL_INFILE is not
+ enabled, as all directories are allowed in that case.
+
- PDO SQLite:
. SQLite's "file:" DSN syntax is now supported, which allows specifying
additional flags. This feature is not available if open_basedir is set.
diff --git a/azure/libmysqlclient_job.yml b/azure/libmysqlclient_job.yml
index 72be92780f..e61b8de95f 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 21fccd415b..2ca344228c 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 "<YourStrong@Passw0rd>" -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;"
diff --git a/ext/mysqli/mysqli.c b/ext/mysqli/mysqli.c
index d4143e5564..bc29c74ce0 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 b968f73567..e1abd07135 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) {
@@ -1674,6 +1674,9 @@ 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)
+ case MYSQL_OPT_LOAD_DATA_LOCAL_DIR:
+#endif
return IS_STRING;
default:
diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c
index 4870a4319a..907b1e3fbc 100644
--- a/ext/mysqli/mysqli_nonapi.c
+++ b/ext/mysqli/mysqli_nonapi.c
@@ -332,6 +332,12 @@ 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 (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:
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 9749fe5632..f1ed103001 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 a39e68b276..b86b58c944 100644
--- a/ext/mysqli/php_mysqli_structs.h
+++ b/ext/mysqli/php_mysqli_structs.h
@@ -46,6 +46,7 @@ typedef _Bool my_bool;
#include <errmsg.h>
#include <mysqld_error.h>
#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 c76e1021e1..19df063951 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 0000000000..56e5e8cdce
--- /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 0000000000..01e79c32a8
--- /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 fef400d0a9..d45d15e6ac 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 0000000000..187701ff8c
--- /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--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=1
+mysqli.local_infile_directory={PWD}/foo/bar
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->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--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->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 6297d56b34..8538fafa4f 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 c2e8aa2dc8..65f4012925 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 0000000000..a9c0c82e99
--- /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--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=0
+mysqli.local_infile_directory={PWD}/foo
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->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--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->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 0000000000..8f50e03220
--- /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--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=0
+mysqli.local_infile_directory={PWD}/foo/bar
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->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--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->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] [2068] LOAD DATA LOCAL INFILE %s
+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 0000000000..a48606ad66
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt
@@ -0,0 +1,65 @@
+--TEST--
+mysqli.local_infile_directory vs open_basedir
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=0
+mysqli.local_infile_directory={PWD}/../
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->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--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->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_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/mysqli/tests/mysqli_phpinfo.phpt b/ext/mysqli/tests/mysqli_phpinfo.phpt
index fd0edd4463..ea78a6bb0c 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 168e161d1b..8730de04b1 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 b65e8523b2..80be26e622 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_LOAD_DATA_LOCAL_INFILE_REJECTED 2068
#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 4cd0433877..c00800c451 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)) {
- SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE,
- "LOAD DATA LOCAL INFILE is forbidden, check mysqli.allow_local_infile");
+ /*
+ 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_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");
+ 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_LOAD_DATA_LOCAL_INFILE_REJECTED, 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_LOAD_DATA_LOCAL_INFILE_REJECTED, 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 75d8af9acd..6ee057fc72 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 8b5577273d..48e47f7871 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 6b42335878..94fa2411d6 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);
}
@@ -724,6 +742,17 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options)
#endif
}
+#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))) {
+ 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 84828edaa4..1bfc5ff874 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 75287e7904..6077ce8245 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 7815b21255..adfd65f5ab 100644
--- a/ext/pdo_mysql/tests/bug70389.phpt
+++ b/ext/pdo_mysql/tests/bug70389.phpt
@@ -26,8 +26,8 @@ var_dump($flags);
array(3) {
[%d]=>
bool(true)
- [1001]=>
+ [%d]=>
bool(true)
- [12]=>
+ [%d]=>
bool(true)
}
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 0000000000..3fa90ba016
--- /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 0000000000..70d8d301e9
--- /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 efbf3c51c8..76db58dff2 100644
--- a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt
+++ b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt
@@ -156,6 +156,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');
+ 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 c9877f3ac1..205e059b54 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')) {
<?php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+ function get_client_version() {
+ if (extension_loaded('mysqli')) {
+ return mysqli_get_client_version();
+ }
+ /* 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. */
+ return MySQLPDOTest::getClientVersion(MySQLPDOTest::factory());
+ }
+
$expected = array(
'MYSQL_ATTR_USE_BUFFERED_QUERY' => true,
'MYSQL_ATTR_LOCAL_INFILE' => true,
@@ -38,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 810adce7e7..9a12f837fb 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--
<?php
@@ -17,8 +20,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 0000000000..edabfbc07f
--- /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--
+<?php
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipifinfilenotallowed.inc');
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
+?>
+--FILE--
+<?php
+ function exec_and_count($offset, &$db, $sql, $exp) {
+ try {
+ $ret = $db->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--
+<?php
+require dirname(__FILE__) . '/mysql_pdo_test.inc';
+$db = MySQLPDOTest::factory();
+$db->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 0000000000..c955c1daad
--- /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--
+<?php
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipifinfilenotallowed.inc');
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
+?>
+--FILE--
+<?php
+ function exec_and_count($offset, &$db, $sql, $exp) {
+ try {
+ $ret = $db->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--
+<?php
+require dirname(__FILE__) . '/mysql_pdo_test.inc';
+$db = MySQLPDOTest::factory();
+$db->exec('DROP TABLE IF EXISTS test');
+?>
+--EXPECTF--
+Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2068 LOAD DATA LOCAL INFILE %s 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 0000000000..c6d60fd1ab
--- /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--
+<?php
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipifinfilenotallowed.inc');
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
+?>
+--FILE--
+<?php
+ function exec_and_count($offset, &$db, $sql, $exp) {
+ try {
+ $ret = $db->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--
+<?php
+require dirname(__FILE__) . '/mysql_pdo_test.inc';
+$db = MySQLPDOTest::factory();
+$db->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 0000000000..abfea299b7
--- /dev/null
+++ b/ext/pdo_mysql/tests/skipifinfilenotallowed.inc
@@ -0,0 +1,6 @@
+<?php
+$db = MySQLPDOTest::factory();
+$stmt = $db->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 2061266e4c..a8f538785b 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 708591bb79..2d6b45d25a 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