Skip to content

Commit

Permalink
Fix #80329: Add option to specify LOAD DATA LOCAL white list folder
Browse files Browse the repository at this point in the history
 * 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 phpGH-6448.

Co-authored-by: Nikita Popov <[email protected]>
  • Loading branch information
marinesovitch and nikic committed Feb 23, 2021
1 parent 7f8ea83 commit da011a3
Show file tree
Hide file tree
Showing 40 changed files with 743 additions and 26 deletions.
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions azure/libmysqlclient_job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions azure/setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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;"
Expand Down
7 changes: 6 additions & 1 deletion ext/mysqli/mysqli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
/* }}} */

Expand All @@ -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;
}
/* }}} */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 4 additions & 1 deletion ext/mysqli/mysqli_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions ext/mysqli/mysqli_nonapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion ext/mysqli/mysqli_prop.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions ext/mysqli/php_mysqli_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ typedef _Bool my_bool;
#include <errmsg.h>
#include <mysqld_error.h>
#include "mysqli_libmysql.h"

#endif /* MYSQLI_USE_MYSQLND */


Expand Down Expand Up @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions ext/mysqli/tests/bug77956.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions ext/mysqli/tests/foo/bar/bar.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
97
98
99
3 changes: 3 additions & 0 deletions ext/mysqli/tests/foo/foo.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1
2
3
12 changes: 10 additions & 2 deletions ext/mysqli/tests/local_infile_tools.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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'";

Expand All @@ -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";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions ext/mysqli/tests/mysqli_constants.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions ext/mysqli/tests/mysqli_local_infile_default_off.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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!
80 changes: 80 additions & 0 deletions ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit da011a3

Please sign in to comment.