php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #47444 Security issue: allow_url_fopen/allow_url_include are useless
Submitted: 2009-02-18 21:19 UTC Modified: 2009-02-19 16:46 UTC
From: the_djmaze at hotmail dot com Assigned:
Status: Not a bug Package: Streams related
PHP Version: 5.2.9RC2 OS: GNU/Linux
Private report: No CVE-ID: None
View Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
If you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: the_djmaze at hotmail dot com
New email:
PHP Version: OS:

 

 [2009-02-18 21:19 UTC] the_djmaze at hotmail dot com
Description:
------------
I already know this for years but as of now no-one reported it so i will.

You can override the security settings of allow_url_fopen and allow_url_include by using the following functions:

http://php.net/stream_wrapper_register
http://php.net/stream_wrapper_unregister

Due to this you can unregister the HTTP wrapper and register your own.
Then with fsockopen or cURL inside that wrapper you completely override the security settings.

Reproduce code:
---------------
Wrapper class: http://dragonflycms.org/cvs/html/includes/classes/http_wrapper.php?v=1.1

<?php
if (!ini_get('allow_url_fopen') && !ini_get('allow_url_include'))
{
	# Force allow_url_fopen=on and allow_url_include=off
	stream_wrapper_unregister('http');
	require('http_wrapper.php');
	stream_wrapper_register('http', 'moo_stream_wrapper_http');
}

getimagesize('http://www.php.net/images/php.gif');
?>

Expected result:
----------------
Warning: getimagesize() [function.getimagesize]: URL file-access is disabled in the server configuration

Warning: getimagesize(http://www.php.net/images/php.gif) [function.getimagesize]: failed to open stream: no suitable wrapper could be found

Warning: getimagesize() [function.getimagesize]: URL file-access is disabled in the server configuration

Warning: getimagesize(http://www.php.net/images/php.gif) [function.getimagesize]: failed to open stream: no suitable wrapper could be found

Actual result:
--------------
success!

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2009-02-18 21:22 UTC] jani@php.net
Thank you for this bug report. To properly diagnose the problem, we
need a short but complete example script to be able to reproduce
this bug ourselves. 

A proper reproducing script starts with <?php and ends with ?>,
is max. 10-20 lines long and does not require any external 
resources such as databases, etc. If the script requires a 
database to demonstrate the issue, please make sure it creates 
all necessary tables, stored procedures etc.

Please avoid embedding huge scripts into the report.



 [2009-02-18 21:34 UTC] the_djmaze at hotmail dot com
i did write 10-20 lines. But all other 200 lines will follow below then:

<?php
class moo_stream_wrapper_http
{
    protected $fullurl;
    protected $p_url;
    protected $conn_id;
    protected $flushed;
    protected $mode = 4; # read only
    protected $defmode;
    protected $redirects = 0;
    protected $binary;
    protected $options;
    protected $stat = array(
        'dev' => 0,
        'ino' => 0,
        'mode' => 0,
        'nlink' => 1,
        'uid' => 0,
        'gid' => 0,
        'rdev' => -1,
        'size' => 0,
        'atime' => 0,
        'mtime' => 0,
        'ctime' => 0,
        'blksize' => -1,
        'blocks' => 0
    );

    protected function error($msg='not connected')
    {
        if ($this->options & STREAM_REPORT_ERRORS) { trigger_error($msg, E_USER_WARNING); }
        return false;
    }

    public function stream_open($path, $mode, $options, $opened_path)
    {
        $dbg = debug_backtrace();
        switch ($dbg[1]['function'])
        {
        case 'include':
        case 'include_once':
        case 'require':
        case 'require_once':
            trigger_error($dbg[1]['function'].'() URL file-access is disabled', E_USER_WARNING);
            return false;
        }
        $this->fullurl = $path;
        $this->options = $options;
        $this->defmode = $mode;

        $url = parse_url($path);
        if (empty($url['host'])) { return $this->error('missing host name'); }
        $this->conn_id = fsockopen($url['host'], (empty($url['port']) ? 80 : intval($url['port'])), $errno, $errstr, 2);
        if (!$this->conn_id) { return false; }
        if (empty($url['path'])) { $url['path'] = '/'; }
        $this->p_url = $url;
        $this->flushed = false;
        if ('r' !== $mode[0] || (strpos($mode, '+') !== false)) { $this->mode += 2; }
        $this->binary = (strpos($mode, 'b') !== false);
        $c = $this->context();
        if (!isset($c['method']))        { stream_context_set_option($this->context, 'http', 'method', 'GET'); }
        if (!isset($c['header']))        { stream_context_set_option($this->context, 'http', 'header', ''); }
        if (!isset($c['user_agent']))    { stream_context_set_option($this->context, 'http', 'user_agent', ini_get('user_agent')); }
        if (!isset($c['content']))       { stream_context_set_option($this->context, 'http', 'content', ''); }
        if (!isset($c['max_redirects'])) { stream_context_set_option($this->context, 'http', 'max_redirects', 5); }
        return true;
    }
    public function stream_close()
    {
        if ($this->conn_id)
        {
            fclose($this->conn_id);
            $this->conn_id = null;
        }
    }
    public function stream_read($bytes)
    {
        if (!$this->conn_id) { return $this->error(); }
        if (!$this->flushed && !$this->stream_flush()) { return false; }
        if (feof($this->conn_id)) { return ''; }
        $bytes = max(1,$bytes);
        if ($this->binary) {
            return fread($this->conn_id, $bytes);
        } else {
            return fgets($this->conn_id, $bytes);
        }
    }
    public function stream_write($data)
    {
        if (!$this->conn_id) { return $this->error(); }
        if (!$this->mode & 2) { return $this->error('Stream is in read-only mode'); }
        $c = $this->context();
        stream_context_set_option($this->context, 'http', 'method', (('x' === $this->defmode[0]) ? 'PUT' : 'POST'));
        if (stream_context_set_option($this->context, 'http', 'content', $c['content'].$data)) { return strlen($data); }
        return 0;
    }
    public function stream_eof()
    {
        if (!$this->conn_id) { return true; }
        if (!$this->flushed) { return false; }
        return feof($this->conn_id);
    }
    public function stream_seek($offset, $whence)
    {
        trigger_error("stream_seek($offset, $whence) not yet supported");
        return false;
    }
    public function stream_tell()
    {
        trigger_error("stream_tell() not yet supported");
        return 0;
    }
    public function stream_flush()
    {
        if ($this->flushed) { return false; }
        if (!$this->conn_id) { return $this->error(); }
        $c = $this->context();
        # send the headers
        $this->flushed = true;
        $RequestHeaders = array(
            $c['method'].' '.$this->p_url['path'].(empty($this->p_url['query']) ? '' : '?'.$this->p_url['query']).' HTTP/1.1',
            'HOST: '.$this->p_url['host'],
            'User-Agent: '.$c['user_agent'].' StreamReader'
        );
        if (!empty($c['header'])) { $RequestHeaders[] = $c['header']; }
        if (!empty($c['content'])) {
            # http://utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html
            if ('PUT' === $c['method']) {
                $RequestHeaders[] = 'Content-Type: '.($this->binary ? 'application/octet-stream' : 'text/plain');
            } else {
                $RequestHeaders[] = 'Content-Type: application/x-www-form-urlencoded';
            }
            $RequestHeaders[] = 'Content-Length: '.strlen($c['content']);
        }
        $RequestHeaders[] = 'Connection: close';
        if (fwrite($this->conn_id, implode("\r\n", $RequestHeaders)."\r\n\r\n") === false) { return false; }
        # send the post data
        if (!empty($c['content']) && fwrite($this->conn_id, $c['content']) === false) { return false; }
        # Get response headers
        global $http_response_header;
        $http_response_header = array(fgets($this->conn_id, 300));
        # Check Status Code w3.org/Protocols/rfc2616/rfc2616-sec10.html
        $data = rtrim($http_response_header[0]);
        preg_match('#.* ([0-9]+) (.*)#i', $data, $head);
        # 301 Moved Permanently, 302 Found, 303 See Other, 307 Temporary Redirect
        if (($head[1] >= 301 && $head[1] <= 303) || $head[1] == 307) {
            $data = rtrim(fgets($this->conn_id, 300)); # read next line
            while (!empty($data)) {
                if (stripos($data, 'Location: ') !== false) {
                    $new_location = trim(str_ireplace('Location: ', '', $data));
                    break;
                }
                $data = rtrim(fgets($this->conn_id, 300)); # read next line
            }
            trigger_error($this->fullurl.' '.$head[2].': '.$new_location, E_USER_NOTICE);
            $this->stream_close();
            return ($c['max_redirects'] > $this->redirects++ && $this->stream_open($new_location, $this->defmode, $this->options, null) && $this->stream_flush());
        }
        # Read all headers
        $data = rtrim(fgets($this->conn_id, 1024)); # read line
        while (!empty($data)) {
            $http_response_header[] = $data."\r\n";
            if (stripos($data, 'Content-Length: ') !== false)    { $this->stat['size']  = trim(str_ireplace('Content-Length: ', '', $data)); }
            else if (stripos($data, 'Date: ') !== false)          { $this->stat['atime'] = strtotime(str_ireplace('Date: ', '', $data)); }
            else if (stripos($data, 'Last-Modified: ') !== false) { $this->stat['mtime'] = strtotime(str_ireplace('Last-Modified: ', '', $data)); }
            $data = rtrim(fgets($this->conn_id, 1024)); # read next line
        }
        # Client/Server error
        if ($head[1] >= 400) {
            trigger_error($this->fullurl.' '.$head[2], E_USER_WARNING);
            return false;
        }
        # file modified?
        if ($head[1] == 304) {
            trigger_error($this->fullurl.' '.$head[2], E_USER_NOTICE);
            return false;
        }
        return true;
    }
    public function stream_stat()
    {
        $this->stream_flush();
        return $this->stat;
    }
    public function dir_opendir($path, $options) { return false; }
    public function dir_readdir() { return ''; }
    public function dir_rewinddir() { return ''; }
    public function dir_closedir() { return; }
    public function url_stat($path, $flags) { return array(); }

    protected function context()
    {
        if (!$this->context) { $this->context = stream_context_create(); }
        $c = stream_context_get_options($this->context);
        return (isset($c['http']) ? $c['http'] : array());
    }
}
?>
 [2009-02-19 00:59 UTC] scottmac@php.net
It disables fopen from using a url, you use fsockopen within a wrap around class with a strema registered on http.

The allow_url_include is the same issue, these ini settings were designed to block drive-by abuse, where a user had failed to sanitize something correctly.

Nothing to fix here as its working as advertised.
 [2009-02-19 16:46 UTC] the_djmaze at hotmail dot com
block drive-by abuse?

1. hosts start using allow_url_fopen=off for "security" reasons
2. people start to use above mentioned way to get around it
3. Wouldn't that make the whole option useless?

If so, you should delete this bug report or it might bring people to bad ideas by not fixing their scripts and use the wrapper.
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sat Dec 21 17:01:58 2024 UTC