PHP Bugs  
php.net | support | documentation | report a bug | advanced search | search howto | statistics | login

go to bug id or search bugs for  

Bug #28932 glob() in safe mode only checks UID for the first file matched by pattern
Submitted:26 Jun 2004 7:36am UTC Modified: 6 Feb 2005 11:34pm UTC
From:php at ter dot dk Assigned to:iliaa
Status:Bogus Category:Safe Mode/open_basedir
Version:4CVS, 5CVS (2005-02-03) OS:*
Votes:7 Avg. Score:5.0 ± 0.0 Reproduced:3 of 3 (100.0%)
Same Version:4 (133.3%) Same OS:2 (66.7%)
View/Vote Developer Edit Submission

[26 Jun 2004 7:36am UTC] php at ter dot dk
Description:
------------
The SAFE_MODE handling of glob() needs a checkup for security reasons.

In short - always with SAFE_MODE on:

1) glob() can still fetch all filenames in a directory not owned by the
same UID as the user, if just the first file in the directory (or more
specific, the glob-pattern) happens to be owned by the same user as the
PHP-script.

2a) No warning is raised if glob is used on another owner's directory
and there is no match.

2b) In those cases where SAFE_MODE correctly prohibits glob() from
fetching a list of files, the warning still discloses the first
filename.

Solution: glob() in SAFE_MODE should be restricted in the same way as
opendir() is.

Longer version + rationale:

1) 
I assume the premise for glob() should be the same as opendir(). But
glob() isn't handled in the same way.

It appears that glob() only checks *the first file* (assuming the
directory isn't owned by the same user) in the glob()-result for
ordinary SAFE_MODE UID-comparsion.

If we have three files in a directory (owned by xyzzy, UID 999):

a.txt (owned by user foo, UID 1000)
b.txt (owned by user bar, UID 1001)
c.txt (owned by user baz, UID 1002)

.. a PHP-script owned by foo executing glob("/path/to/directory/*") will
return all file names, regardless of the ownership (of course, the
Apache User would have to have read/execute-access to the directory)

The exact same PHP-script owned by bar will give an error, just because
the first file in the directory doesn't happen to be owned by bar.

2a)
glob() is allowed to check whether no files match a specific pattern.

2b)
Furthermore, the glob() SAFE_MODE warning (when present) disclose the
first file name (where the UID check is performed):

opendir("/path/to/directory/") raises a warning like "The script whose
uid is 1001 is not allowed to access /path/to/directory/ owned by uid
999". This is fine.

glob("/path/to/directory/*") raises a warning like "The script whose uid
is 1001 is not allowed to access /path/to/directory/a.txt owned by uid
1000". Notice that "a.txt" is mentioned in the error - a file name we
might not have known in advance.

Combining the problems mentioned in 2a and 2b gives us a possibility to
get an almost complete list of files in a directory by walking through
the letters, as in this example:

("Does a* exists? No. Does b* exist? No. Does c* exist? Yes, cyber.txt
exists. Does cz* exist? No. Does d* exist? Yes, door.php exists. Does
dp* exist? No. Does dq* exist? No.")

Of course, instead of "Yes, cyber.txt exists", one would recieve ".. not
allowed to access cyber.txt", still disclosing the name.

Reproduce code:
---------------
Preparation:

1. touch /tmp/a as the same user as the owner of the following PHP-code
(create the file out of PHP - yes, this might require shell access in
this setup, but this is just a controlled preparation to highlight the
problem)

2. make sure PHP is running in safe_mode

3. Test code:

// Example 1
$a = glob("/tmp/*");

// Example 2a
$b = glob("/tmp/doesnotexist*");

// Example 2b
$c = glob("/tmp/sess_*");

Expected result:
----------------
In all cases:

Warning: glob(): SAFE MODE Restriction in effect. The script whose uid
is 1000 is not allowed to access /tmp/ owned by uid 0

Actual result:
--------------
For $a:
glob() works unrestricted and a list of all file names is retrieved.

For $b:
glob() works unrestricted and returns false (giving us the knowledge
that no files begin with "doesnotexist").

For $c:
"Warning: glob(): SAFE MODE Restriction in effect. The script whose uid
is 1000 is not allowed to access
/tmp/sess_14758f1afd44c09b7992073ccf00b43d owned by uid 33" (disclosing
the session file name [33 is the Apache User])

[28 Jun 2004 5:36am UTC] php at ter dot dk
As a sidenote related to session security this bug could in a default
setup with multiple virtual hosts in safe mode (as in a typical
webhosting-setup) be exploited pretty bad by a single customer,
retrieving ALL current sessionids:

1. Create a folder which is world-writable
2. Create a PHP-script that writes to a new .php-file in that folder
(making that file having the same user as the Apache user), using
something like glob(ini_get("session.save_path")."/sess_*")
3. Access that PHP-script via a browser.

Since the script is owned by the same UID as the Apache-user and the
session-files, and glob() checks the UID for the first file (where it
instead only should check the UID for the directory), a full list of
*all* session files is available - even sessions for sites under other
virtual hosts.

Combined with the possible exploit mentioned in bug #28242 (online test
at http://stock.ter.dk/session.php - the bug was dismissed as "not our
problem; every single administrator in the world would just have to
create a custom save_path for each and every virtual host"), the user
could read and write sesssion data to every single session on the
server.

The Filesystem and Security chapter still doesn't mention anything about
the problem, and even the Safe Mode chapter states: " The PHP safe mode
is an attempt to solve the shared-server security problem. It is
architecturally incorrect to try to solve this problem at the PHP level,
but since the alternatives at the web server and OS levels aren't very
realistic, many people, especially ISP's, use safe mode for now." 

I agree that it would be nice if every single administrator would have
separate session.save_path for each virtual host or even jailed every
user, but as mentioned above, it isn't that realistic.

I really hope that one would consider reworking the session storage
process as mentioned in bug #28242, and somewhat creating a harder job
to find or accessing session files

Some approaches to solve parts of the problem could be adding the UID to
the session file in safe mode (as HTTP authentication currently is
doing), the servername, a hash of both, and/or by other means not having
users to be able to access the session files directly (which even could
mean not allowing scripts - still in safe mode - with the same UID as
the Apache user, although some applications might depend on this
functionality with serverside-generated php-code) or otherwise disallow
file related functions from accessing sess_*-files at all - once again,
still only in safe mode.

.. and even if my session-related concern is disregarded, I still hope
the glob()-bug is fixed :)

- Peter Brodersen
[28 Jun 2004 10:04pm UTC] iliaa@php.net
Thank you for taking the time to write to us, but this is not
a bug. Please double-check the documentation available at
http://www.php.net/manual/ and the instructions on how to report
a bug at http://bugs.php.net/how-to-report.php

Checking each file inside a directory would be too slow, 
same thing goes for opendir() & readdir(). Given that you 
just get a file list and no other information or the 
ability to access those file. In this particular case there 
is no loss of security. 
[29 Jun 2004 1:47am UTC] php at ter dot dk
I'm baffled... I really am!

First of all, I propose a change to *one* UID-check on the directory
alone (currently there are *two' checks, whether UID matches on the
directory || the UID matches on the first file). I'm not voting for a
UID check for every single file and I haven't mentioned that as a
solution, so please don't use that argument.

Furthermore, currently safe_mode-users would be able to access every
single session file. Not only the one they created themselves (as
mentioned in bug #28242 ), but each and every single session stored on
the server!

Please answer: What is the rationale for just checking the first file?
There isn't any. It will lead to random, unpredicted results and
confusion.

Safemode has prevented users of accessing any information that they
aren't permitted to. It doesn't make sense deciding globally that "It
wouldn't harm for users to know of files at another host". There is no
reason for providing users in safe mode with this information in the
first place.

What I really would like to see is:

1. Rationale for this random-check at first file. This is slower than
just checking the dir alone, so please no "This is too slow"-argument.
At least remove this check, as it doesn't make any sense.

2. Rationale for not updating the documentation regarding file and
session security. There is no mention at all for using custom
save_paths. If this bug really is "bogus", at least change it to a
documentation issue, instead of hiding important information for the
users. The same problem was mentioned in bug #28242 - the documentation
really is pretty poor on this issue!

3. In summary, as of marking this bug as bogus, please state clearly:
"Yes, a user should be able to see every session file in safe_mode" and
"Yes, a user should be able to figure out every filename on the system
requiring a small amount of work as opposed to brute force".

As mentioned, I'm baffled.

Sincerely,
Peter Brodersen
[29 Jun 2004 5:45pm UTC] iliaa@php.net
Thank you for taking the time to write to us, but this is not
a bug. Please double-check the documentation available at
http://www.php.net/manual/ and the instructions on how to report
a bug at http://bugs.php.net/how-to-report.php

safe_mode is not entirely safe and has many drawbacks. It 
is much better to use open_basedir to restrict the user to 
their home directory or any other set of directories. 
 
As far as glob() goes the check is done on the 1st file, 
since ultimately you get data about the files inside the 
directory and not the directory itself. More over glob 
directory may infact be a pattern and not an actual 
directory, making the check based on that nearly 
impossible. 
[27 Jul 2004 3:54am UTC] php at ter dot dk
I have now performed some tests with open_basedir as you suggested.

Two of the issues (2a: empty glob-match is not restricted, and 2b:
filenames is disclosed in warning) is also present under open_basedir.

Proof of concept:
http://basedir.ter.dk/notexist.php (2a)
http://basedir.ter.dk/nobody.php (2b)

As mentioned in http://news.php.net/php.internals/11578 , even:

- with safe_mode-restriction
- with open_basedir-restriction
- with custom session.save_path for each virtual host/user
- without allowing php-scripts of the same UID as the Apache user to be
executed (mostly because of the possibility of bypassing a
safe_mode-UID-check)

.. a user can still walk around and get info on directory and file names
fairly easy, e.g. finding session files, giving a hijack-opportunity.

In other words: open_basedir will not help us from preventing glob() to
be maliciously used to get information about directory and file names.
This is why I have re-opened the bug: two of the issues is still present
under open_basedir-restriction (although the Summary could be changed to
reflect this).

As a side note, not related to the above reasons for re-opening the
bug:

The retrieval of a file list is usually connected to the permissions for
the directory (e.g. the read-bit for a directory in unix). Following
this logic, the same restriction should be added here. At least that's
the case for opendir().

There are no differences between glob() and opendir(), since the
directory handle from opendir() is only usable by readdir(), that
returns a filename from the directory.

Both functions is used to retrieve filenames from a directory, no more,
no less. Same effect, different approaches.

Futhermore, the first-file-check is still useless, as a similar check
isn't performed on readdir(). If we perform opendir() on our own
directory, the ownership of the files in the directory has no effect on
readdir() - there is no restriction by safe_mode in this case. I could
put up a test case for this too, although it is pretty easy to test
out.

If there are any more suggestions for restrictions I could test (besides
safe_mode, open_basedir, etc.), please let me know :)

Thanks for your patience.

- Peter Brodersen
[3 Feb 2005 5:49am UTC] php at ter dot dk
Sorry - still no luck with that CVS snapshot (4.3.11).

glob() still just check the first file. Example:

$ php4-STABLE-200502030330/sapi/cli/php -d safe_mode=On -r
'print_r(glob("/tmp/*"));'

Warning: glob(): SAFE MODE Restriction in effect.  The script whose uid
is 1000 is not allowed to access /tmp/build owned by uid 33 in Command
line code on line 1

$ touch /tmp/a

$ php4-STABLE-200502030330/sapi/cli/php -d safe_mode=On -r
'print_r(glob("/tmp/*"));'
Array
(
    [0] => /tmp/a
    [1] => /tmp/build
    [2] => /tmp/config
    [3] => /tmp/lib
    [4] => /tmp/magic5su99j
    [5] => /tmp/mailgraph
    [6] => /tmp/out.txt
    [7] => /tmp/phptest_sess_03735f0f339412345678901234567890
    [8] => /tmp/phptest_sess_0e838e6ff9e312345678901234567890
(.. and so on ..)

1) appearently doesn't seem to be fixed. Please don't comment on the
fact that if I have shell access, I have access to those file names
anyway - this was just a quick test :)

As the example above shows, 2b) isn't fixed either. The error (".. is
not allowed to access /tmp/build owned by .."). An error due to
open_basedir-restriction discloses it as well: "open_basedir restriction
in effect. File(/tmp/build) is not within the allowed path(s): .."

So, it still seem that even under open_basedir- and
safe_mode-restriction, the issue is still present.

.. and just to clarify: I do agree that safe_mode is not per se safe,
and all these vulnerabilities would be present if a user had access to a
shell or other languages - but there's no need for further
inconsistency.

I'll e-mail a simple proof-of-concept-code of filepath-walking security@
and sniper@. The code utilizes these warning messages. The code is
pretty simple (not to mention ugly), though.

- Peter Brodersen
[6 Feb 2005 11:34pm UTC] iliaa@php.net
It is not practical to test every single file returned by glob() against
safe_mode & open_basedir, it would terribly slow to check every single
file/directory.

As far as the error message goes, everytime you hit open_basedir limit
the error indicating that path XYZ could not be opened due to
open_basedir/safe_mode restriction.

RSS feed | show source 

PHP Copyright © 2001-2009 The PHP Group
All rights reserved.
Last updated: Sat Nov 21 10:30:49 2009 UTC