Skip to content

Fix #80329: Add option to specify LOAD DATA LOCAL white list folder #6448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -650,6 +652,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 @@ -1069,7 +1074,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
4 changes: 4 additions & 0 deletions ext/mysqli/mysqli_nonapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b
unsigned int allow_local_infile = MyG(allow_local_infile);
mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&allow_local_infile);

#if MYSQL_VERSION_ID > 80021 || defined(MYSQLI_USE_MYSQLND)
mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory));
#endif

end:
if (!mysqli_resource) {
mysqli_resource = (MYSQLI_RESOURCE *)ecalloc (1, sizeof(MYSQLI_RESOURCE));
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 @@ -283,6 +284,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 @@ -201,6 +201,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!
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
Original file line number Diff line number Diff line change
@@ -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] [2036] LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file
done
Loading