Skip to content

Commit da011a3

Browse files
marinesovitchnikic
andcommitted
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>
1 parent 7f8ea83 commit da011a3

40 files changed

+743
-26
lines changed

NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ PHP NEWS
2424
. Fixed bug #70372 (Emulate mysqli_fetch_all() for libmysqlclient). (Nikita)
2525
. Fixed bug #80330 (Replace language in APIs and source code/docs).
2626
(Darek Ślusarczyk)
27+
. Fixed bug #80329 (Add option to specify LOAD DATA LOCAL white list folder
28+
(including libmysql)). (Darek Ślusarczyk)
2729

2830
- Opcache:
2931
. Added inheritance cache. (Dmitry)

UPGRADING

+12
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,18 @@ PHP 8.1 UPGRADE NOTES
176176
Note, that the quality of the custom secret is crucial for the quality of the resulting hash. It is
177177
highly recommended for the secret to use the best possible entropy.
178178

179+
- MySQLi:
180+
. The mysqli.local_infile_directory ini setting has been added, which can be
181+
used to specify a directory from which files are allowed to be loaded. It
182+
is only meaningful if mysqli.allow_local_infile is not enabled, as all
183+
directories are allowed in that case.
184+
185+
- PDO MySQL:
186+
. The PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY attribute has been added, which
187+
can be used to specify a directory from which files are allowed to be
188+
loaded. It is only meaningful if PDO::MYSQL_ATTR_LOCAL_INFILE is not
189+
enabled, as all directories are allowed in that case.
190+
179191
- PDO SQLite:
180192
. SQLite's "file:" DSN syntax is now supported, which allows specifying
181193
additional flags. This feature is not available if open_basedir is set.

azure/libmysqlclient_job.yml

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
set -o
1919
sudo service mysql start
2020
mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS test"
21+
# Ensure local_infile tests can run.
22+
mysql -uroot -proot -e "SET GLOBAL local_infile = true"
2123
displayName: 'Setup MySQL server'
2224
# Does not support caching_sha2_auth :(
2325
#- template: libmysqlclient_test.yml

azure/setup.yml

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ steps:
55
sudo service postgresql start
66
sudo service slapd start
77
mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS test"
8+
# Ensure local_infile tests can run.
9+
mysql -uroot -proot -e "SET GLOBAL local_infile = true"
810
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
911
sudo -u postgres psql -c "CREATE DATABASE test;"
1012
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;"

ext/mysqli/mysqli.c

+6-1
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ PHP_INI_BEGIN()
499499
#endif
500500
STD_PHP_INI_BOOLEAN("mysqli.reconnect", "0", PHP_INI_SYSTEM, OnUpdateLong, reconnect, zend_mysqli_globals, mysqli_globals)
501501
STD_PHP_INI_BOOLEAN("mysqli.allow_local_infile", "0", PHP_INI_SYSTEM, OnUpdateLong, allow_local_infile, zend_mysqli_globals, mysqli_globals)
502+
STD_PHP_INI_ENTRY("mysqli.local_infile_directory", NULL, PHP_INI_SYSTEM, OnUpdateString, local_infile_directory, zend_mysqli_globals, mysqli_globals)
502503
PHP_INI_END()
503504
/* }}} */
504505

@@ -523,6 +524,7 @@ static PHP_GINIT_FUNCTION(mysqli)
523524
mysqli_globals->report_mode = 0;
524525
mysqli_globals->report_ht = 0;
525526
mysqli_globals->allow_local_infile = 0;
527+
mysqli_globals->local_infile_directory = NULL;
526528
mysqli_globals->rollback_on_cached_plink = FALSE;
527529
}
528530
/* }}} */
@@ -600,6 +602,9 @@ PHP_MINIT_FUNCTION(mysqli)
600602
REGISTER_LONG_CONSTANT("MYSQLI_READ_DEFAULT_FILE", MYSQL_READ_DEFAULT_FILE, CONST_CS | CONST_PERSISTENT);
601603
REGISTER_LONG_CONSTANT("MYSQLI_OPT_CONNECT_TIMEOUT", MYSQL_OPT_CONNECT_TIMEOUT, CONST_CS | CONST_PERSISTENT);
602604
REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOCAL_INFILE", MYSQL_OPT_LOCAL_INFILE, CONST_CS | CONST_PERSISTENT);
605+
#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND)
606+
REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOAD_DATA_LOCAL_DIR", MYSQL_OPT_LOAD_DATA_LOCAL_DIR, CONST_CS | CONST_PERSISTENT);
607+
#endif
603608
REGISTER_LONG_CONSTANT("MYSQLI_INIT_COMMAND", MYSQL_INIT_COMMAND, CONST_CS | CONST_PERSISTENT);
604609
REGISTER_LONG_CONSTANT("MYSQLI_OPT_READ_TIMEOUT", MYSQL_OPT_READ_TIMEOUT, CONST_CS | CONST_PERSISTENT);
605610
#ifdef MYSQLI_USE_MYSQLND
@@ -1021,7 +1026,7 @@ void php_mysqli_fetch_into_hash_aux(zval *return_value, MYSQL_RES * result, zend
10211026
MYSQL_ROW row;
10221027
unsigned int i, num_fields;
10231028
MYSQL_FIELD *fields;
1024-
zend_ulong *field_len;
1029+
unsigned long *field_len;
10251030

10261031
if (!(row = mysql_fetch_row(result))) {
10271032
RETURN_NULL();

ext/mysqli/mysqli_api.c

+4-1
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,7 @@ PHP_FUNCTION(mysqli_fetch_lengths)
12101210
#ifdef MYSQLI_USE_MYSQLND
12111211
const size_t *ret;
12121212
#else
1213-
const zend_ulong *ret;
1213+
const unsigned long *ret;
12141214
#endif
12151215

12161216
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)
16731673
case MYSQL_SET_CHARSET_DIR:
16741674
#if MYSQL_VERSION_ID > 50605 || defined(MYSQLI_USE_MYSQLND)
16751675
case MYSQL_SERVER_PUBLIC_KEY:
1676+
#endif
1677+
#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND)
1678+
case MYSQL_OPT_LOAD_DATA_LOCAL_DIR:
16761679
#endif
16771680
return IS_STRING;
16781681

ext/mysqli/mysqli_nonapi.c

+6
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,12 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b
332332
unsigned int allow_local_infile = MyG(allow_local_infile);
333333
mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&allow_local_infile);
334334

335+
#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND)
336+
if (MyG(local_infile_directory) && !php_check_open_basedir(MyG(local_infile_directory))) {
337+
mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory));
338+
}
339+
#endif
340+
335341
end:
336342
if (!mysqli_resource) {
337343
mysqli_resource = (MYSQLI_RESOURCE *)ecalloc (1, sizeof(MYSQLI_RESOURCE));

ext/mysqli/mysqli_prop.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ static int result_lengths_read(mysqli_object *obj, zval *retval, bool quiet)
267267
#ifdef MYSQLI_USE_MYSQLND
268268
const size_t *ret;
269269
#else
270-
const zend_ulong *ret;
270+
const unsigned long *ret;
271271
#endif
272272
uint32_t field_count;
273273

ext/mysqli/php_mysqli_structs.h

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ typedef _Bool my_bool;
4646
#include <errmsg.h>
4747
#include <mysqld_error.h>
4848
#include "mysqli_libmysql.h"
49+
4950
#endif /* MYSQLI_USE_MYSQLND */
5051

5152

@@ -276,6 +277,7 @@ ZEND_BEGIN_MODULE_GLOBALS(mysqli)
276277
char *default_pw;
277278
zend_long reconnect;
278279
zend_long allow_local_infile;
280+
char *local_infile_directory;
279281
zend_long strict;
280282
zend_long error_no;
281283
char *error_msg;

ext/mysqli/tests/bug77956.phpt

+2-3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,5 @@ $link->close();
5555
unlink('bug77956.data');
5656
?>
5757
--EXPECTF--
58-
Warning: mysqli::query(): LOAD DATA LOCAL INFILE forbidden in %s on line %d
59-
[006] [2000] LOAD DATA LOCAL INFILE is forbidden, check mysqli.allow_local_infile
60-
done
58+
[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
59+
done

ext/mysqli/tests/foo/bar/bar.data

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
97
2+
98
3+
99

ext/mysqli/tests/foo/foo.data

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
1
2+
2
3+
3

ext/mysqli/tests/local_infile_tools.inc

+10-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
}
77
}
88

9-
function check_local_infile_support($link, $engine, $table_name = 'test') {
10-
9+
function check_local_infile_allowed_by_server($link) {
1110
if (!$res = mysqli_query($link, 'SHOW VARIABLES LIKE "local_infile"'))
1211
return "Cannot check if Server variable 'local_infile' is set to 'ON'";
1312

@@ -16,6 +15,15 @@
1615
if ('ON' != $row['Value'])
1716
return sprintf("Server variable 'local_infile' seems not set to 'ON', found '%s'", $row['Value']);
1817

18+
return "";
19+
}
20+
21+
function check_local_infile_support($link, $engine, $table_name = 'test') {
22+
$res = check_local_infile_allowed_by_server($link);
23+
if ($res) {
24+
return $res;
25+
}
26+
1927
if (!mysqli_query($link, sprintf('DROP TABLE IF EXISTS %s', $table_name))) {
2028
return "Failed to drop old test table";
2129
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
--TEST--
2+
mysqli.allow_local_infile overrides mysqli.local_infile_directory
3+
--SKIPIF--
4+
<?php
5+
require_once('skipif.inc');
6+
require_once('skipifconnectfailure.inc');
7+
8+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
9+
die("skip Cannot connect to MySQL");
10+
11+
include_once("local_infile_tools.inc");
12+
if ($msg = check_local_infile_allowed_by_server($link))
13+
die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
14+
15+
mysqli_close($link);
16+
17+
?>
18+
--INI--
19+
open_basedir={PWD}
20+
mysqli.allow_local_infile=1
21+
mysqli.local_infile_directory={PWD}/foo/bar
22+
--FILE--
23+
<?php
24+
require_once("connect.inc");
25+
26+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
27+
printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
28+
}
29+
30+
if (!$link->query("DROP TABLE IF EXISTS test")) {
31+
printf("[002] [%d] %s\n", $link->errno, $link->error);
32+
}
33+
34+
if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) {
35+
printf("[003] [%d] %s\n", $link->errno, $link->error);
36+
}
37+
38+
$filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
39+
if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
40+
printf("[004] [%d] %s\n", $link->errno, $link->error);
41+
}
42+
43+
if ($res = mysqli_query($link, 'SELECT COUNT(id) AS num FROM test')) {
44+
$row = mysqli_fetch_assoc($res);
45+
mysqli_free_result($res);
46+
47+
$row_count = $row['num'];
48+
$expected_row_count = 3;
49+
if ($row_count != $expected_row_count) {
50+
printf("[005] %d != %d\n", $row_count, $expected_row_count);
51+
}
52+
} else {
53+
printf("[006] [%d] %s\n", $link->errno, $link->error);
54+
}
55+
56+
$link->close();
57+
echo "done";
58+
?>
59+
--CLEAN--
60+
<?php
61+
require_once('connect.inc');
62+
63+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
64+
printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
65+
$host, $user, $db, $port, $socket);
66+
}
67+
68+
if (!$link->query($link, 'DROP TABLE IF EXISTS test')) {
69+
printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link));
70+
}
71+
72+
$link->close();
73+
?>
74+
--EXPECT--
75+
done

ext/mysqli/tests/mysqli_constants.phpt

+4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ mysqli.allow_local_infile=1
202202
$expected_constants["MYSQLI_TYPE_JSON"] = true;
203203
}
204204

205+
if ($version > 80210 || $IS_MYSQLND) {
206+
$expected_constants['MYSQLI_OPT_LOAD_DATA_LOCAL_DIR'] = true;
207+
}
208+
205209
$unexpected_constants = array();
206210

207211
foreach ($constants as $group => $consts) {

ext/mysqli/tests/mysqli_local_infile_default_off.phpt

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ echo "server: ", $row['Value'], "\n";
1616
mysqli_free_result($res);
1717
mysqli_close($link);
1818

19-
echo "connector: ", ini_get("mysqli.allow_local_infile"), "\n";
19+
echo 'connector: ', ini_get('mysqli.allow_local_infile'), ' ', var_export(ini_get('mysqli.local_infile_directory')), "\n";
2020

2121
print "done!\n";
2222
?>
2323
--EXPECTF--
2424
server: %s
25-
connector: 0
25+
connector: 0 ''
2626
done!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
--TEST--
2+
mysqli.local_infile_directory vs access allowed
3+
--SKIPIF--
4+
<?php
5+
require_once('skipif.inc');
6+
require_once('skipifconnectfailure.inc');
7+
8+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
9+
die("skip Cannot connect to MySQL");
10+
11+
include_once("local_infile_tools.inc");
12+
if ($msg = check_local_infile_allowed_by_server($link))
13+
die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
14+
15+
mysqli_close($link);
16+
17+
?>
18+
--INI--
19+
open_basedir={PWD}
20+
mysqli.allow_local_infile=0
21+
mysqli.local_infile_directory={PWD}/foo
22+
--FILE--
23+
<?php
24+
require_once("connect.inc");
25+
26+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
27+
printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
28+
}
29+
30+
if (!$link->query("DROP TABLE IF EXISTS test")) {
31+
printf("[002] [%d] %s\n", $link->errno, $link->error);
32+
}
33+
34+
if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) {
35+
printf("[003] [%d] %s\n", $link->errno, $link->error);
36+
}
37+
38+
$filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
39+
if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
40+
printf("[004] [%d] %s\n", $link->errno, $link->error);
41+
}
42+
43+
$filepath = str_replace('\\', '/', __DIR__.'/foo/bar/bar.data');
44+
if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
45+
printf("[005] [%d] %s\n", $link->errno, $link->error);
46+
}
47+
48+
if ($res = mysqli_query($link, 'SELECT COUNT(id) AS num FROM test')) {
49+
$row = mysqli_fetch_assoc($res);
50+
mysqli_free_result($res);
51+
52+
$row_count = $row['num'];
53+
$expected_row_count = 6;
54+
if ($row_count != $expected_row_count) {
55+
printf("[006] %d != %d\n", $row_count, $expected_row_count);
56+
}
57+
} else {
58+
printf("[007] [%d] %s\n", $link->errno, $link->error);
59+
}
60+
61+
$link->close();
62+
echo "done";
63+
?>
64+
--CLEAN--
65+
<?php
66+
require_once('connect.inc');
67+
68+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
69+
printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
70+
$host, $user, $db, $port, $socket);
71+
}
72+
73+
if (!$link->query($link, 'DROP TABLE IF EXISTS test')) {
74+
printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link));
75+
}
76+
77+
$link->close();
78+
?>
79+
--EXPECT--
80+
done

0 commit comments

Comments
 (0)