]> granicus.if.org Git - php/commitdiff
Fix #80329: Add option to specify LOAD DATA LOCAL white list folder
authorDarek Slusarczyk <dariusz.slusarczyk@oracle.com>
Mon, 22 Feb 2021 10:03:24 +0000 (11:03 +0100)
committerNikita Popov <nikita.ppv@gmail.com>
Tue, 23 Feb 2021 08:30:46 +0000 (09:30 +0100)
 * 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>
40 files changed:
NEWS
UPGRADING
azure/libmysqlclient_job.yml
azure/setup.yml
ext/mysqli/mysqli.c
ext/mysqli/mysqli_api.c
ext/mysqli/mysqli_nonapi.c
ext/mysqli/mysqli_prop.c
ext/mysqli/php_mysqli_structs.h
ext/mysqli/tests/bug77956.phpt
ext/mysqli/tests/foo/bar/bar.data [new file with mode: 0644]
ext/mysqli/tests/foo/foo.data [new file with mode: 0644]
ext/mysqli/tests/local_infile_tools.inc
ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt [new file with mode: 0644]
ext/mysqli/tests/mysqli_constants.phpt
ext/mysqli/tests/mysqli_local_infile_default_off.phpt
ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt [new file with mode: 0644]
ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt [new file with mode: 0644]
ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt [new file with mode: 0644]
ext/mysqli/tests/mysqli_phpinfo.phpt
ext/mysqlnd/mysqlnd_connection.c
ext/mysqlnd/mysqlnd_enum_n_def.h
ext/mysqlnd/mysqlnd_loaddata.c
ext/mysqlnd/mysqlnd_structs.h
ext/pdo_mysql/config.w32
ext/pdo_mysql/mysql_driver.c
ext/pdo_mysql/pdo_mysql.c
ext/pdo_mysql/php_pdo_mysql_int.h
ext/pdo_mysql/tests/bug70389.phpt
ext/pdo_mysql/tests/foo/bar/bar.data [new file with mode: 0644]
ext/pdo_mysql/tests/foo/foo.data [new file with mode: 0644]
ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt
ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt
ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt
ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt [new file with mode: 0644]
ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt [new file with mode: 0644]
ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt [new file with mode: 0644]
ext/pdo_mysql/tests/skipifinfilenotallowed.inc [new file with mode: 0644]
php.ini-development
php.ini-production

diff --git a/NEWS b/NEWS
index 4020fd7bb8330a3f36a0a70c737e4758238f41e6..02e3c77cf92891c6eacf2689807de20662975b25 100644 (file)
--- 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)
index eb01058a5ab7a4d98ccb0170d4cb5dedae8408a2..f5c00596e4d00cb190541dd167b10a98d058e103 100644 (file)
--- 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.
index 72be92780fb6d307e9ff1fad478399ad8d8495ab..e61b8de95ff9d21fe499eca5f591edb26a78ebf7 100644 (file)
@@ -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
index 21fccd415b5b4789e5d626439c117a4272b0ad68..2ca344228cdba60aa040c2540e5347e0626abd47 100644 (file)
@@ -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;"
index d4143e55649a81ef12debe03fbd58bb20a0be02e..bc29c74ce0300dc58ef2769f5917085a19d8a733 100644 (file)
@@ -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();
index b968f735673dacf5a4f327488b3733ee8aa0024b..e1abd07135c68e17c842fe1ab63df4d9fac1e1c4 100644 (file)
@@ -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;
 
index 4870a4319a7bf6b42f32dd8d959167da2ab23f88..907b1e3fbc92dbda5228e70b2cf513bb30473ebe 100644 (file)
@@ -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));
index 9749fe5632556c986fc029ca4d5d7cd8496f3c9d..f1ed103001daa91258571fe9d1d000a069729d9f 100644 (file)
@@ -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;
 
index a39e68b2761228b7ed7cd46e5c6d35a7cea80064..b86b58c9444e041ec039e6febb95759144128885 100644 (file)
@@ -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;
index c76e1021e1586a98821f4e87c6a5b208bb8f85fb..19df063951cc04605768940c2288189ae7e63a2a 100644 (file)
@@ -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 (file)
index 0000000..56e5e8c
--- /dev/null
@@ -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 (file)
index 0000000..01e79c3
--- /dev/null
@@ -0,0 +1,3 @@
+1
+2
+3
index fef400d0a907fb99bed40b651371b1a3941b12d4..d45d15e6ac3c26546457a28c4b190082ecc136a7 100644 (file)
@@ -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'";
 
         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 (file)
index 0000000..187701f
--- /dev/null
@@ -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
index 6297d56b34033f33994e150cefb265c2cae419f2..8538fafa4ff240efe6e5d8a911d245ef81012274 100644 (file)
@@ -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) {
index c2e8aa2dc80eafe745d1818838ff3cb153ea1adf..65f40129250e31c9c739a1047a5433511781c022 100644 (file)
@@ -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 (file)
index 0000000..a9c0c82
--- /dev/null
@@ -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 (file)
index 0000000..8f50e03
--- /dev/null
@@ -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 (file)
index 0000000..a48606a
--- /dev/null
@@ -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
index fd0edd4463ab04449135c7be0215e7a4bf590dbf..ea78a6bb0c101917dc9dfc4a1dde4f5037ba62a6 100644 (file)
@@ -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)
index 168e161d1bd77778c12e9857dc47970ba904776c..8730de04b1f7b8384067bc8e6ca14bced5d52a60 100644 (file)
@@ -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;
index b65e8523b2a85b30d17aa69ba016e541ae3af143..80be26e6226b1edb502406f84528b5a0c9c5bb8d 100644 (file)
 #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,
index 4cd04338777b950a31b77a48cd2ce3776dfb0450..c00800c451d97de49868f3b5d78ab812bc947791 100644 (file)
@@ -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;
index 75d8af9acd28068298294932d0e5d947ac7b2747..6ee057fc724feb73e793d10a85bdfff57d278cf6 100644 (file)
@@ -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;
 
 
index 8b5577273db88006796d823b4c286d77d183841d..48e47f78718667e5adb40c7b8943d3a4c2748e17 100644 (file)
@@ -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");
index 6b42335878da89adde52bf48d50d66b4a39faace..94fa2411d67a4d032336d2550df1014e0c700afe 100644 (file)
@@ -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
index 84828edaa45aee28672b488efd8ded42d9fe8fd1..1bfc5ff8747f5f92278bc09bb56f0ba84155dd02 100644 (file)
@@ -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);
index 75287e7904cb9c70e09b5d88d957bbdb5432a864..6077ce8245b0b7d75a0f4ead62b18e2bd69b65d6 100644 (file)
@@ -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
index 7815b21255740f8443ce37201cf5cd9fffdedd76..adfd65f5ab1f4c66a47e6941a2b90d79ec5855c6 100644 (file)
@@ -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 (file)
index 0000000..3fa90ba
--- /dev/null
@@ -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 (file)
index 0000000..70d8d30
--- /dev/null
@@ -0,0 +1,3 @@
+1;one
+2;two
+3;three
index efbf3c51c812cb0790f0fc587d9911149135e73f..76db58dff202e89c5a7afdffe960e5a4edd57765 100644 (file)
@@ -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(),
index c9877f3ac12dc3f5a58289dae122fda57abd2953..205e059b5418449cc287e13f0f610a72c3f4170d 100644 (file)
@@ -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;
     }
 
     /*
index 810adce7e7b7f82502d70cd9593f00d60ca2b624..9a12f837fb7ae82fcbce022f94510d40917a6630 100644 (file)
@@ -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 (file)
index 0000000..edabfbc
--- /dev/null
@@ -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 (file)
index 0000000..c955c1d
--- /dev/null
@@ -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 (file)
index 0000000..c6d60fd
--- /dev/null
@@ -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 (file)
index 0000000..abfea29
--- /dev/null
@@ -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'] ."'");
+?>
index 2061266e4c8850715213927c5917915b1cbd8e6f..a8f538785be7d473b265560bd84fada0f1318b62 100644 (file)
@@ -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
index 708591bb797a79c359c0603b52edd3e10d474df2..2d6b45d25a1dfad1036f6d35d2176c77b759f429 100644 (file)
@@ -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