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>
This commit is contained in:
Darek Slusarczyk 2021-02-22 11:03:24 +01:00 committed by Nikita Popov
parent 7f8ea83ef4
commit da011a312a
40 changed files with 743 additions and 26 deletions

2
NEWS
View file

@ -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)

View file

@ -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.

View 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

View 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;"

View 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();

View 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;

View 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));

View 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;

View 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;

View 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

View file

@ -0,0 +1,3 @@
97
98
99

View file

@ -0,0 +1,3 @@
1
2
3

View 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'";
@ -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";
}

View file

@ -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

View 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) {

View 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!

View file

@ -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

View file

@ -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

View file

@ -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

View 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)

View 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;

View file

@ -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,

View 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;

View 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;

View 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");

View 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

View 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);

View 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

View file

@ -26,8 +26,8 @@ var_dump($flags);
array(3) {
[%d]=>
bool(true)
[1001]=>
[%d]=>
bool(true)
[12]=>
[%d]=>
bool(true)
}

View file

@ -0,0 +1,3 @@
97;first
98;second
99;third

View file

@ -0,0 +1,3 @@
1;one
2;two
3;three

View 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(),

View 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;
}
/*

View 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!

View file

@ -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!

View file

@ -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!

View file

@ -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!

View file

@ -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'] ."'");
?>

View 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

View 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