PHP `require_once` includes wrong file

875 views Asked by At

I have a development tree on a Linux Ubuntu 14.04-LTS machine like this, with three identical branches:

main -+-- leonardo --- project --- htdocs -+- panel --- index.php
      |                                    |
      |                                    +- config.php
      |
      +-- federico --- project --- htdocs -+- panel --- index.php
      |                                    |
      |                                    +- config.php
      |
      +-- carlo ------ project --- htdocs -+- panel --- index.php
      |                                    |
      |                                    +- config.php
    ..... (you get my drift).

There are neither soft links nor hard links. The config.php file is in svn-ignore and is different between all branches

There is an Apache server and there is a virtualHost for each developer, so I can see my development version at http://leonardo.project.local or Federico's at http://federico.project.local .

While investigating the current weirdness, the two files are these:

<?php // this is panel/index.php
    echo "I am " . __FILE__ . "\n";
    echo "I will include " . realpath('../config.php') . "\n";
    require_once '../config.php';

<?php // this is config.php
    echo "I am " . __FILE__ . "\n";
    exit();

The expected output of course would be:

I am leonardo/project/htdocs/panel/index.php
I will include /var/www/main/leonardo/project/htdocs/config.php
I am leonardo/project/htdocs/config.php

But the actual output is:

I am leonardo/project/htdocs/panel/index.php
I will include /var/www/main/leonardo/project/htdocs/config.php
I am federico/project/htdocs/config.php

The additional weirdness is that

    echo "I will include " . realpath('../config.php') . "\n";
    require_once realpath('../config.php');

works.

TL;DR require_once and realpath disagree about where '../config.php' actually is.

The really strange thing is that I do not see how a script running in leonardo/project/htdocs/panel/ could know about federico/project/htdocs/config.php; it ought to go four directories up, then explore very many subdirectories.

I'm almost beginning to suspect that this could be something filesystem- or even kernel- related.

The filesystem is ext4, the kernel is 3.13.0-55-generic #92-Ubuntu SMP Sun Jun 14 18:32:20 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux. The machine is a virtual x64 on the latest VMware Workstation.

Checks

  • PHP's include_path only includes . and /usr/local/php5/pear.
  • as stated earlier, no files in the branch are symlinks, and the inode counts for all involved files indicate there are no cross links. The files are indeed different.
  • all files are really there, it's not a "last ditch include".
  • from command line, in leonardo...panel, I run "cat ../config.php" and I get my config.php, as expected. It is only from PHP that the wrong file gets included.
  • restarting Apache (just in case) availed nothing. I'll next try and reboot the whole VM, but to do that I need to freeze several services and it will take me a while.
  • everything was hunky dory up to yesterday (I wasn't here then). There were no system updates, no reboots, and not even remote logins in the last three days. Uptime is now eight days.
  • I'm an idiot: I can too know to the minute when this started happening by checking the integration test logs. Have asked for them, expecting them after lunch.
2

There are 2 answers

0
Blizz On BEST ANSWER

Have you checked any potential opcode caches and their settings? In the past I have had some issues there, eg not detecting changed files.

Specifically, this situation can and will happen if opcache.use_cwd setting is set to zero.

opcache.use_cwd boolean

If enabled, OPcache appends the current working directory to the script key, thereby eliminating possible collisions between files with the same base name. Disabling this directive improves performance, but may break existing applications.

If this happens, then the first user or phpunit script accessing a file of a given name in a different directory (e.g. leonardo/config.php vs federico/config.php) will "prime" the cache with that file. The file system functions such as realpath will not be affected and will continue working. References using absolute paths will continue working. References with relative paths will be broken in a quite insidious way.

For until you have only one person working, that person has the cache primed for his needs and will notice nothing. Then you come back to work, and you start loading his files.

On a side note, the setting might cause unintentional information disclosure, because the setting is system wide. So you know that your ISP has a broken use_cwd, you know that another site includes '../inc/credit_cards.php', you prepare the same path in your site and include a file with the same name. get_defined_vars() might have you pwn the other site's login or database system. (Haven't checked, but given what happened, don't see why not).

UPDATE

I checked with the above configuration on PHP5.6 and an old backed up VM (we have time-machine snapshots of the last five years :-) ). I was indeed able to read all defined global variables in a different virtual host to which I had no access, overriding authentication. This problem was discovered in 2016 but the tests they thought of did not extend to tricking opcache into including some one else's file by creating a file with the same name.

I have also re-run the same test on our current dev VM, and the problem appears to have gone away, even if the configuration has changed so much that I'm not sure they're comparable anymore.

I am considering renaming shared-host config.inc.php files to something like config.a72b1qTy.inc.php.

1
Jim On

It's worth checking what your include_path is set to (this can be done using get_include_path).

require and include will behave differently given an absolute and relative path. When you use

require_once realpath('../config.php');

This is doing:

require_once '/var/www/main/leonardo/project/htdocs/config.php';

Which works as you'd expect.

The weirdness in the following:

require_once '../config.php';

occurs because PHP will check each entry in the include path for a matching file and return the first matching entry. Hence it's likely that the path to the federico config is being checked first.