|  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #69209 Path resolution inconsistency
Submitted: 2015-03-09 22:56 UTC Modified: 2015-05-27 00:50 UTC
From: ak4t0sh at free dot fr Assigned:
Status: Not a bug Package: Streams related
PHP Version: Irrelevant OS: Linux
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.
Block user comment
Status: Assign to:
Bug Type:
From: ak4t0sh at free dot fr
New email:
PHP Version: OS:


 [2015-03-09 22:56 UTC] ak4t0sh at free dot fr

During a code review i found a strange behaviour in most of php function that deal with path handling.

Indeed I found a code that required a file this way : `require __DIR__."../../include/myfile.php";` building a path like like `/path/to../../include/myfile.php` instead of the right path `/path/to/../include/myfile.php`
But strangely the file was correctly loaded by require...
As you can see when you execute test script in the "normal case" the file is loaded even if file_exists() return false...

The others parts of the test script are here as a POC to demonstrate that such behaviour could lead to a LFI exploitation. To test them remove the exit at line 22.
Indeed as the file `/path/to/../include/myfile.php` is loaded without any error or warning when calling `/path/to../../include/myfile.php` the developer could miss the coding error.
But if an attacker could create a symlink `/path/to.` which target an especially crafted file tree that could allow him to read / write / execute others (private) files. (LFI)
In fact this is not a very easy LFI to exploit but in my case the file was a template file so as an attacker a possible exploitation would be to replace the loaded stream by my own (malicious) code without editing the original source code.

The problem seems to be located to the "../../" part only.
Indeed the following paths logically failed :

After some researches i suppose that the problem is in "zend_resolve_path" function but as i'm not familiar with php internal code i prefer let the experts look and decide if it's a bug or a feature ;)

This has been tested on default installation of PHP 5.6.5, 5.4.36 and PHP 5.3.3 on Debian Jessie, Wheezy and Squeeze

Arnaud Trouvé (ak4t0sh)

Test script:
chmod +x
mv poc.txt poc.php

Expected result:
cf test script comments

Actual result:
cf test script comments


Add a Patch

Pull Requests

Add a Pull Request


AllCommentsChangesGit/SVN commitsRelated reports
 [2015-03-10 15:54 UTC] ak4t0sh at free dot fr
All things considered, that vulnerability is not a LFI (indeed there is no injection in code) but it's more like a stream redirection
 [2015-03-10 22:54 UTC]
-Status: Open +Status: Not a bug
 [2015-03-10 22:54 UTC]
There's no error here. Path /path/to../../include/myfile.php is the same as /path/include/myfile.php as .. in the file/directory name is completely valid and .. as always removes previous component.
 [2015-03-12 11:47 UTC] ak4t0sh at free dot fr
-Summary: Path resolution error with trailing dotted directory name +Summary: Path resolution inconsistency
 [2015-03-12 11:47 UTC] ak4t0sh at free dot fr
ok thanks for the explanation i was wrong there is no problem with dot character handling i thought there would a check from the entire path validity before its resolution.

However i think there is a real problem in path resolving consistency between file_exists/stream_resolve_include_path/realpath and fopen/file_get_contents/file_put_contents/require/include...(others)
Indeed the fact is in the test case 2 (aka WTF : single dotted path in test file) file_exists/stream_resolve_include_path/realpath are all returning false but we can still open file using fopen, etc...

Here a smaller script to test it : 
file_put_contents(__DIR__."/foo/bar.php", "content");
var_dump(file_exists(__DIR__."/foo/bar.php"), , realpath(__DIR__."/foo/bar.php"), stream_resolve_include_path(__DIR__."/foo/bar.php"), file_get_contents(__DIR__."/foo/bar.php"));
var_dump(file_exists(__DIR__."/inexistant_dir/../foo/bar.php"), realpath(__DIR__."/inexistant_dir/../foo/bar.php"), stream_resolve_include_path(__DIR__."/inexistant_dir/../foo/bar.php"), file_get_contents(__DIR__."/inexistant_dir/../foo/bar.php"));
So on one hand i have file_exists (and others functions) which return me the the file "inexistant_dir/../foo/bar.php" does not exists and on the other hand i can still open it using file_get_contents...
So if i follow your reasoning about ".." that remove the previous component of path we can not really rely on file_exists because in this script it should have remove "inexistant_dir" from path and so return true. Same for realpath and stream_resolve_include_path which should have return __DIR__."/foo/bar.php" instead of false.

So theorically i can write a code like this (i know it's stupid - it's for demo)
file_put_contents(__DIR__."/foo/bar.php", "content");
if (file_exists(__DIR__."/inexistant_dir/../foo/bar.php") == false)
    echo file_get_contents(__DIR__."/inexistant_dir/../foo/bar.php");    #does not exists ? i don't care open it anyway

As there is no problem with dot character i renamed this issue.

Is there an explanation to such inconsistency ? If not as i cannot do it i let you reopen the issue and remove the security flag because i think this should be fixed or documented at least.
 [2015-05-26 14:59 UTC] ak4t0sh at free dot fr

Any feedback about such inconsistency ?


Arnaud Trouvé (ak4t0sh)
 [2015-05-27 00:50 UTC]
-Type: Security +Type: Bug
PHP Copyright © 2001-2020 The PHP Group
All rights reserved.
Last updated: Sat Jul 04 10:01:25 2020 UTC