ID: 47444 User updated by: the_djmaze at hotmail dot com Reported By: the_djmaze at hotmail dot com Status: Bogus Bug Type: Streams related Operating System: GNU/Linux PHP Version: 5.2.9RC2 New Comment:
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. Previous Comments: ------------------------------------------------------------------------ [2009-02-19 00:59:09] [email protected] 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-18 21:34:59] 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-18 21:22:48] [email protected] 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:19:06] 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! ------------------------------------------------------------------------ -- Edit this bug report at http://bugs.php.net/?id=47444&edit=1
