Page MenuHomePhabricator

Disallow MYSQLI_OPT_LOCAL_INFILE
Closed, ResolvedPublic

Description

A malicious MySQL server can instruct MySQL clients to send it any file. See:

This is not normally exploitable (an attacker can't instruct Phabricator to connect to a MySQL server they control) but the existence of the capability is completely absurd and it's relatively easy to prevent:

https://github.com/vrana/adminer/commit/c564a8ef5095d26b1d7f2ebab4dc3c3516bc0c7b

It looks like the approach is:

  • If we're using MySQLi, disable MYSQLI_OPT_LOCAL_INFILE.
  • If we're not using MySQLI, raise a setup warning if mysql.allow_local_infile is enabled.

Event Timeline

epriestley triaged this task as Low priority.Jan 18 2019, 4:01 PM
epriestley created this task.

It looks like we don't need to do anything about mysql on the CLI since this option is, thankfully, not enabled by default:

For the mysql client, local data loading is disabled by default. To disable or enable it explicitly, use the --local-infile=0 or --local-infile[=1] option.
https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html

If this wasn't the case, I'd also want to update the cluster configs to disable this for the mysql CLI client.

I'm unable to get the MySQLi option MYSQLI_OPT_LOCAL_INFILE to actually work. Here's the script I'm using:

<?php

require_once 'scripts/init/init-script.php';

$conn = id(new PhabricatorUser())->establishConnection('r');

queryfx(
  $conn,
  'CREATE TEMPORARY TABLE tmp (v text)');

queryfx(
  $conn,
  'LOAD DATA LOCAL INFILE %s INTO TABLE tmp',
  'README.md');

$result = queryfx_all(
  $conn,
  'SELECT * FROM tmp');

var_dump($result);

This works (loads the local file) even if I set MYSQLI_OPT_LOCAL_INFILE!

Maybe it "knows" that the local and remote are really the same host?


Another attack here is that a user who finds SQL injection can LOAD DATA LOCAL INFILE ... to read data from the web host into the database, then SELECT it back. This can be prevented by disabling local_infile on the server. I'll add setup guidance for this, too.

I can't get MYSQLI_OPT_LOCAL_INFILE to work on secure, either. I tried on secure001 and secure004 (where the database is not local). As far as I can tell, this option doesn't do anything, anywhere, ever? I'm going to look at the source and see if I can figure out what's going on, but I'll back it out of D19998 if I can't find some evidence that it's useful.

I think that maybe mysql_nonapi.c just overrides the conn->options() call? Near line 269 of PHP 7.2.3:

ext/mysqli/mysqli_nonapi.c
mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&MyG(allow_local_infile));

My theory here is that conn->connect() is overwriting MYSQL_OPT_LOCAL_INFILE with the value of mysqli.allow_local_infile, so setting MYSQL_OPT_LOCAL_INFILE never has any effect. Not entirely sure that's true and I'm not ambitious enough about figuring this out to recompile PHP to test it, but maybe it's plausible?

Maybe another point in favor of this claim is that the option does not work is the behavior of this:

// With: mysqli.allow_local_infile = 0
$conn->options(MYSQLI_OPT_LOCAL_INFILE, 1);

...does not allow LOAD DATA INFILE LOCAL, but I can't find any code in the source that explicitly does something that looks like "make the config option stronger than the options() call".

epriestley added a revision: Restricted Differential Revision.Jan 18 2019, 5:40 PM
epriestley added a commit: Restricted Diffusion Commit.Jan 19 2019, 4:03 AM
epriestley lowered the priority of this task from Low to Wishlist.Jan 20 2019, 3:14 PM

We're probably done here, but ideally the next steps are:

  • Confirm that MYSQLI_OPT_LOCAL_INFILE actually has the bug I describe above (i.e., it is always overwritten by mysqli.allow_local_infile and never has any effect).
  • Report this to the PHP upstream. At time of writing, I wasn't able to find a similar report on bugs.php.net.

Since I probably have to build PHP from source at some point for T13232 anyway, it's at least plausible that I'll get to this.

vrana added a subscriber: vrana.Jan 21 2019, 8:27 AM

I can't get MYSQLI_OPT_LOCAL_INFILE to work on secure, either. I tried on secure001 and secure004 (where the database is not local). As far as I can tell, this option doesn't do anything, anywhere, ever?

You have to set it after the real_connect call, not before it. It is weird and counter-intuitive. I verified that my Adminer fix really fixed the issue (and external researcher verified it too) but I remember having the same problem of this apparently doing nothing.

It looks like we don't need to do anything about mysql on the CLI since this option is, thankfully, not enabled by default:

For the mysql client, local data loading is disabled by default. To disable or enable it explicitly, use the --local-infile=0 or --local-infile[=1] option.
https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html

This can be set also in the my.cnf file so it's probably worth checking or setting explicitly.

Thanks! I get the same behavior locally, I filed this upstream: https://bugs.php.net/bug.php?id=77496