php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #54110 tsrm_realpath_r and junction point with denied read access
Submitted: 2011-02-27 12:05 UTC Modified: 2017-10-24 07:33 UTC
Votes:1
Avg. Score:4.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:1 (100.0%)
Same OS:1 (100.0%)
From: carsten_sttgt at gmx dot de Assigned:
Status: Open Package: Filesystem function related
PHP Version: Irrelevant OS: Windows
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: carsten_sttgt at gmx dot de
New email:
PHP Version: OS:

 

 [2011-02-27 12:05 UTC] carsten_sttgt at gmx dot de
Description:
------------
tsrm_realpath_r is having a problem if working with/inside a junction point and
"read data/list directory" is denied on this junction point.


That's the implementation for many common directories on Windows7/Vista in e.g. %USERPROFILE%, %ALLUSERSPROFILE% like "%USERPROFILE%\SendTo".


The problem is a call to CreateFile with GENERIC_READ access in tsrm_realpath_r. Well, the MSDN is writing:
> If this parameter is zero, the application can query certain metadata
> such as file, directory, or device attributes without accessing that
> file or device, even if GENERIC_READ access would have been denied.

I think "zero" is save at this point and that's what I'm doing in my patch.

Regards,
Carsten





Test script:
---------------
in the shell:
md test1
mklink /j test test1
icacls test /deny *S-1-1-0:(rd)
php -r "var_dump(realpath('test'));"
php -r "var_dump(fopen('test/test.txt', 'w'));"



Expected result:
----------------
string(31) "C:\Users\Public\Documents\test1"

resource(5) of type (stream)


Actual result:
--------------
bool(false)

Warning: fopen(test/test.txt): failed to open stream: No such file or directory
in Command line code on line 1


Patches

TSRM-tsrm_virtual_cwd.c.diff (last revision 2011-02-27 11:06 UTC) by carsten_sttgt at gmx dot de)

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2011-02-27 12:15 UTC] pajoye@php.net
-Status: Open +Status: Feedback
 [2011-02-27 12:15 UTC] pajoye@php.net
I'm not sure your diagnostic is correct. Junction or links are supported and work 
well. Is /deny RD not about denying read data/list directory? Then why should we 
do it?
 [2011-02-27 12:18 UTC] pajoye@php.net
Keeping in mind that PHP does not allow one to access data if it does not have the 
permission to do it (like reading meta outside openbasedir, or similar cases).
 [2011-02-27 12:54 UTC] carsten_sttgt at gmx dot de
> I'm not sure your diagnostic is correct.
It's no problem to do a:
echo foo>test\test.txt

but it's not possible to do a:
php -r "file_put_contents('test/test.txt', 'foo');"

"dir test" is not possible because "list directory" is denied. (This deny is only for the junction object itself (only this dir), but not for objects inside the junction target.)
Thus, "md test1\test2 && dir test\test2" would work.
 


> Junction or links are supported and work well.
Well, it's working with a symlink, but not with a junction point.
BTW: realpath() on a non existent junction target is also not working as expected. The output is the name of non existent junction point target. With a non existent symlink it's the expected false.
 [2011-02-27 15:24 UTC] pajoye@php.net
I don't agree. Junction does work very well.

The exact case you are using may not work but I'm somehow not convinced that we 
should allow that now. We had similar cases where someone wanted to access stat 
info from an unreadable file.
 [2011-02-27 16:58 UTC] carsten_sttgt at gmx dot de
> Junction does work very well.

You have tested this?
| mklink test1 nonexistent
| mklink /j test2 nonexistent
| php -r "var_dump(realpath('test1'));"
| php -r "var_dump(realpath('test2'));"
What did you expect? (You can also test it with the sample exe below)


> We had similar cases where someone wanted to access stat 
> info from an unreadable file.

It's a difference, if generic read access is denied, or data read access. This still allows me to other things with an object, e.g reading attributes or permissions...


> I'm somehow not convinced that we should allow that now.

PHP can't allow me, what is not allowed/possible from the ACL. It's just working wrong in this piece of code. Because:
- I have full rights on e.g. test1\test.php
- But PHP don't let me do anything with this file
That's just wrong.

Maybe we should think about what this function tsrm_realpath_r is doing. It's just resolving a relative path to an absolute one. And it's testing if the target exists. Nothing else. It's something like:
| #include <windows.h>
| #include <tchar.h>
| #include <stdio.h>
| 
| void _tmain(int argc, TCHAR *argv[]) {
|     HANDLE hFile;
|     TCHAR  buffer[MAX_PATH]=TEXT(""); 
| 
|     if (GetFullPathName(argv[1], MAX_PATH, buffer, NULL)) {
|         hFile = CreateFile(
|             buffer, 0, 0, NULL, OPEN_EXISTING,
|             FILE_FLAG_BACKUP_SEMANTICS, NULL
|         );
|         if (hFile != INVALID_HANDLE_VALUE) { 
|             _tprintf(TEXT("The full path name is:  %s\n"), buffer);
|             CloseHandle(hFile);
|             exit(EXIT_SUCCESS);
|         }
|     }
|     _tprintf(TEXT("GetFullPathName failed (%d)\n"), GetLastError());
|     exit(EXIT_FAILURE);
| }
(ok, this simple example does not resolve the final pathname from a symlink)

Such a function must not have GENERIC_READ as DesiredAccess, because it doesn't want read any data from any object. Especially it doesn't know what I want do with this file(name) at another place. The final access control and opening the handle for the real work is done at another place.

And of course. If generic access is denied from the ACL, CreateFile with "0" also fail. (you can't do things which are not allowed...)
 [2011-02-27 19:50 UTC] pajoye@php.net
-Status: Feedback +Status: Assigned -Assigned To: +Assigned To: pajoye
 [2011-02-27 19:50 UTC] pajoye@php.net
It is not only about windows (sometimes very confusing) ways to configure ACL but portability in the way permissions or what we allow or not work.

I'm also not sure that what you proposed does not break more that what it solves. 
I will double check that later in March.
 [2011-02-27 19:51 UTC] pajoye@php.net
"Maybe we should think about what this function tsrm_realpath_r is doing. It's 
just resolving a relative path to an absolute one. And it's testing if the target 
exists. Nothing else"

It does not only do that and it is used for more than that.
 [2011-02-28 17:32 UTC] carsten_sttgt at gmx dot de
> I'm also not sure that what you proposed does not break more
> that what it solves. I will double check that later in March.

That's ok. BTW, this will also fix Bug #50659 (but read below). In the meantime: Maybe you want add "FILE_SHARE_READ as 3rd Parameters for CreateFile (to prevent a race condition).


> It does not only do that and it is used for more than that.
Oh, look like complicated code for doing some task without description what the function is doing ;-)

Well, it's resolving a relative path to an absolute one, testing if the target 
exists. And it's resolving symlinks along the path. What else?
(really a pity that GetFinalPathNameByHandle only exists as of Vista.)

Especially the last (and because it's called in most functions, although the functions can use relative paths directly.) raises some problems. I guess that's for this openbasedir check?

Well, according to #50659 I have a real live issue for a similar reason (and why I'm using ASP for one project and not PHP):

Given a WEBSERVER which is using a share "Files" on FILESERVER (//FILESERVER/Files).

"Files" is the directory "D:\Files". Some Videos are located at "E:\Videos" and there is a junction from "D:\Files\Videos" to "E:\Videos".

Ok, now on WEBSERVER I'm using PHP to access "//FILESERVER/Files/Videos/". What is PHP doing?
It's resolving the last junction to "E:\Videos" and thus it's searching rhis directory on WEBSERVER. Still curious this doesn't happen with every function:
"passtru('//FILESERVER/Files/Videos/foo.mpg');" is working.
"$f=fopen('//FILESERVER/Files/Videos/foo.mpg', 'rb'); fpassthru($f);" not.

Well, I guess resolving symlinks should only be done on local drives (or let CreateFile in fopen doing this job itself).
 [2011-02-28 17:38 UTC] pajoye@php.net
The main problem we have is about the incldue/require _once function, 
permissions issues (no meta read if no permission (as in no read)), etc.

I'm working on droping almost all useless path resolution for (f)open and 
related functions. They should do nothing but create the file or the file 
handle, instead of checking each part of the request path.

But that's a very (very) sensible part of php and I'm reluctant to make changes 
too quickly in this area for an edge case. At least not without clear test 
cases.

We have a bunch of tests covering this code but it takes some time to run them, 
fix, re test, etc. So don't hold your breath, it won't happen before 5.3.6 is 
released :)

Thanks for your feedback!
 [2011-03-02 19:44 UTC] carsten_sttgt at gmx dot de
OK and sorry. So let me add this to this topic...
Assuming a default Windows/IIS installation:
| <?php
| $temp = tmpfile();
| $filedata = stream_get_meta_data($temp);
| var_dump(realpath($filedata['uri']));
| ?>

The result:
| boolean false

But it's a little bit different to the problem above. fopen() etc. is working. Only realpath() fails. And the patch above doesn't fix this issue.
 [2012-03-02 14:49 UTC] ab@php.net
After taking a look at the stuff I've realized that there is a FindFirstFile 
invocation in TSRM/tsrm_virtual_cwd.c around line 852. There is a check for 
INVALID_HANDLE_VALUE after which "return -1" happens. If I do the following 
after FindFirstFile fails 

DWORD dw = GetLastError();

if (ERROR_ACCESS_DENIED == dw) {
	/* inside junction ? */
}

I see that it happens for exactly the case trying to read "test\test.txt" from 
the initial example. A 'not found' status is given for nonexistent files

A solution I'm thinking about is to traverse all the parents until the first 
junction, after that the junction target can be resolved and the complete real 
path could be built. This could be done with the check above for this specific 
case. Not sure if there would be impacts on dependant functionality
 [2017-10-24 07:33 UTC] kalle@php.net
-Status: Assigned +Status: Open -Assigned To: pajoye +Assigned To:
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Sun Nov 19 01:31:42 2017 UTC