Commit f2b9b4f6 authored by zw's avatar zw

11

parent 9fa9f03f
# Change Log
PHP Curl Class uses semantic versioning with version numbers written as `MAJOR.MINOR.PATCH`. You may safely update
`MINOR` and `PATCH` version changes. It is recommended to review `MAJOR` changes prior to upgrade as there may be
backwards-incompatible changes that will affect existing usage.
### Changes
(TODO: Add changes for next `MAJOR` version release.)
### Manual Review
Manually view changes on the [comparison page](https://github.com/php-curl-class/php-curl-class/compare/). For example,
visit [7.4.0...8.0.0](https://github.com/php-curl-class/php-curl-class/compare/7.4.0...8.0.0) to compare the changes for
the `MAJOR` upgrade from 7.4.0 to 8.0.0. Comparing against `HEAD` is also possible using the `tag...HEAD` syntax
([8.3.0...HEAD](https://github.com/php-curl-class/php-curl-class/compare/8.3.0...HEAD)).
View the log between releases:
$ git fetch --tags
$ git log 7.4.0...8.0.0
View the code changes between releases:
$ git fetch --tags
$ git diff 7.4.0...8.0.0
View only the source log and code changes between releases:
$ git log 7.4.0...8.0.0 "src/"
$ git diff 7.4.0...8.0.0 "src/"
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
This diff is collapsed.
# Security Considerations
### Url may point to system files
* Don't blindly accept urls from users as they may point to system files. Curl supports many protocols including `FILE`.
The following would show the contents of `file:///etc/passwd`.
```bash
# Attacker.
$ curl https://www.example.com/display_webpage.php?url=file%3A%2F%2F%2Fetc%2Fpasswd
```
```php
// display_webpage.php
$url = $_GET['url']; // DANGER!
$curl = new Curl();
$curl->get($url);
echo $curl->response;
```
Safer:
```php
function is_allowed_url($url, $allowed_url_schemes = array('http', 'https')) {
$valid_url = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED) !== false;
if ($valid_url) {
$scheme = parse_url($url, PHP_URL_SCHEME);
return in_array($scheme, $allowed_url_schemes, true);
}
$valid_ip = filter_var($url, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
return $valid_ip;
}
$url = $_GET['url'];
if (!is_allowed_url($url)) {
die('Unsafe url detected.');
}
```
### Url may point to internal urls
* Url may point to internal urls including those behind a firewall (e.g. http://192.168.0.1/ or ftp://192.168.0.1/). Use
a whitelist to allow certain urls rather than a blacklist.
### Request data may refer to system files
* Request data prefixed with the `@` character may have special interpretation and read from system files.
```bash
# Attacker.
$ curl https://www.example.com/upload_photo.php --data "photo=@/etc/passwd"
```
```php
// upload_photo.php
$curl = new Curl();
$curl->post('http://www.anotherwebsite.com/', array(
'photo' => $_POST['photo'], // DANGER!
));
```
### Unsafe response with redirection enabled
* Requests with redirection enabled may return responses from unexpected sources.
Downloading https://www.example.com/image.png may redirect and download https://www.evil.com/virus.exe
```php
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); // DANGER!
$curl->download('https://www.example.com/image.png', 'my_image.png');
```
```php
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); // DANGER!
$curl->get('https://www.example.com/image.png');
```
### Keep SSL protections enabled
* Do not disable SSL protections.
```php
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // DANGER!
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // DANGER!
```
### Prevent XML External Entity injection
* Set the following when using the default PHP XML parser to prevent XML external entity injection.
```php
libxml_disable_entity_loader(true);
```
### Prevent PHP execution of library files
PHP files in this library are not intended to be accessible by users browsing websites. Prevent direct access to library files by moving the library folder at least one level higher than the web root directory. Alternatively, configure the server to disable php file execution for all library files.
#### For WordPress plugin developers
WordPress plugin developers that wish to incorporate the PHP Curl Class library into their plugin, should take special care to include only the "core" library files.
Do one of the following:
Option 1. Download an official release from the [releases page](https://github.com/php-curl-class/php-curl-class/releases) and incorporate the files contained in the compressed file into the plugin. The releases include only the necessary php files for the library to function.
Option 2. Manually copy only the [src/](https://github.com/php-curl-class/php-curl-class/tree/master/src) directory into your plugin. Be sure not to copy any other php files as they may be executable by users visiting the php files directly.
{
"name": "php-curl-class/php-curl-class",
"description": "PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs.",
"homepage": "https://github.com/php-curl-class/php-curl-class",
"license": "Unlicense",
"keywords": [
"php", "curl", "class", "api", "api-client", "client", "framework", "http", "http-client", "http-proxy", "json",
"php-curl", "php-curl-library", "proxy", "requests", "restful", "web-scraper", "web-scraping", "web-service",
"xml"
],
"authors": [
{
"name": "Zach Borboa"
}
],
"require": {
"php": ">=5.3",
"ext-curl": "*"
},
"require-dev": {
"ext-gd": "*",
"phpunit/phpunit": "*",
"squizlabs/php_codesniffer": "*"
},
"suggest": {
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"Curl\\": "src/Curl/"
}
}
}
<?php
namespace Curl;
class ArrayUtil
{
/**
* Is Array Assoc
*
* @access public
* @param $array
*
* @return boolean
*/
public static function is_array_assoc($array)
{
return (bool)count(array_filter(array_keys($array), 'is_string'));
}
/**
* Is Array Multidim
*
* @access public
* @param $array
*
* @return boolean
*/
public static function is_array_multidim($array)
{
if (!is_array($array)) {
return false;
}
return (bool)count(array_filter($array, 'is_array'));
}
/**
* Array Flatten Multidim
*
* @access public
* @param $array
* @param $prefix
*
* @return array
*/
public static function array_flatten_multidim($array, $prefix = false)
{
$return = array();
if (is_array($array) || is_object($array)) {
if (empty($array)) {
$return[$prefix] = '';
} else {
foreach ($array as $key => $value) {
if (is_scalar($value)) {
if ($prefix) {
$return[$prefix . '[' . $key . ']'] = $value;
} else {
$return[$key] = $value;
}
} else {
if ($value instanceof \CURLFile) {
$return[$key] = $value;
} else {
$return = array_merge(
$return,
self::array_flatten_multidim(
$value,
$prefix ? $prefix . '[' . $key . ']' : $key
)
);
}
}
}
}
} elseif ($array === null) {
$return[$prefix] = $array;
}
return $return;
}
/**
* Array Random
*
* @access public
* @param $array
*
* @return mixed
*/
public static function array_random($array)
{
return $array[mt_rand(0, count($array) - 1)];
}
}
<?php
namespace Curl;
class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
{
/**
* @var mixed[] Data storage with lowercase keys.
* @see offsetSet()
* @see offsetExists()
* @see offsetUnset()
* @see offsetGet()
* @see count()
* @see current()
* @see next()
* @see key()
*/
private $data = array();
/**
* @var string[] Case-sensitive keys.
* @see offsetSet()
* @see offsetUnset()
* @see key()
*/
private $keys = array();
/**
* Construct
*
* Allow creating an empty array or converting an existing array to a
* case-insensitive array. Caution: Data may be lost when converting
* case-sensitive arrays to case-insensitive arrays.
*
* @param mixed[] $initial (optional) Existing array to convert.
*
* @return CaseInsensitiveArray
*
* @access public
*/
public function __construct(array $initial = null)
{
if ($initial !== null) {
foreach ($initial as $key => $value) {
$this->offsetSet($key, $value);
}
}
}
/**
* Offset Set
*
* Set data at a specified offset. Converts the offset to lowercase, and
* stores the case-sensitive offset and the data at the lowercase indexes in
* $this->keys and @this->data.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetset.php
*
* @param string $offset The offset to store the data at (case-insensitive).
* @param mixed $value The data to store at the specified offset.
*
* @return void
*
* @access public
*/
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->data[] = $value;
} else {
$offsetlower = strtolower($offset);
$this->data[$offsetlower] = $value;
$this->keys[$offsetlower] = $offset;
}
}
/**
* Offset Exists
*
* Checks if the offset exists in data storage. The index is looked up with
* the lowercase version of the provided offset.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php
*
* @param string $offset Offset to check
*
* @return bool If the offset exists.
*
* @access public
*/
public function offsetExists($offset)
{
return (bool) array_key_exists(strtolower($offset), $this->data);
}
/**
* Offset Unset
*
* Unsets the specified offset. Converts the provided offset to lowercase,
* and unsets the case-sensitive key, as well as the stored data.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php
*
* @param string $offset The offset to unset.
*
* @return void
*
* @access public
*/
public function offsetUnset($offset)
{
$offsetlower = strtolower($offset);
unset($this->data[$offsetlower]);
unset($this->keys[$offsetlower]);
}
/**
* Offset Get
*
* Return the stored data at the provided offset. The offset is converted to
* lowercase and the lookup is done on the data store directly.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetget.php
*
* @param string $offset Offset to lookup.
*
* @return mixed The data stored at the offset.
*
* @access public
*/
public function offsetGet($offset)
{
$offsetlower = strtolower($offset);
return isset($this->data[$offsetlower]) ? $this->data[$offsetlower] : null;
}
/**
* Count
*
* @see https://secure.php.net/manual/en/countable.count.php
*
* @param void
*
* @return int The number of elements stored in the array.
*
* @access public
*/
public function count()
{
return (int) count($this->data);
}
/**
* Current
*
* @see https://secure.php.net/manual/en/iterator.current.php
*
* @param void
*
* @return mixed Data at the current position.
*
* @access public
*/
public function current()
{
return current($this->data);
}
/**
* Next
*
* @see https://secure.php.net/manual/en/iterator.next.php
*
* @param void
*
* @return void
*
* @access public
*/
public function next()
{
next($this->data);
}
/**
* Key
*
* @see https://secure.php.net/manual/en/iterator.key.php
*
* @param void
*
* @return mixed Case-sensitive key at current position.
*
* @access public
*/
public function key()
{
$key = key($this->data);
return isset($this->keys[$key]) ? $this->keys[$key] : $key;
}
/**
* Valid
*
* @see https://secure.php.net/manual/en/iterator.valid.php
*
* @return bool If the current position is valid.
*
* @access public
*/
public function valid()
{
return (bool) !(key($this->data) === null);
}
/**
* Rewind
*
* @see https://secure.php.net/manual/en/iterator.rewind.php
*
* @param void
*
* @return void
*
* @access public
*/
public function rewind()
{
reset($this->data);
}
}
This diff is collapsed.
<?php
namespace Curl;
class Decoder
{
/**
* Decode JSON
*
* @access public
* @param $json
* @param $assoc
* @param $depth
* @param $options
*/
public static function decodeJson()
{
$args = func_get_args();
// Call json_decode() without the $options parameter in PHP
// versions less than 5.4.0 as the $options parameter was added in
// PHP version 5.4.0.
if (version_compare(PHP_VERSION, '5.4.0', '<')) {
$args = array_slice($args, 0, 3);
}
$response = call_user_func_array('json_decode', $args);
if ($response === null) {
$response = $args['0'];
}
return $response;
}
/**
* Decode XML
*
* @access public
* @param $data
* @param $class_name
* @param $options
* @param $ns
* @param $is_prefix
*/
public static function decodeXml()
{
$args = func_get_args();
$response = @call_user_func_array('simplexml_load_string', $args);
if ($response === false) {
$response = $args['0'];
}
return $response;
}
}
<?php
namespace Curl;
class Encoder
{
/**
* Encode JSON
*
* Wrap json_encode() to throw error when the value being encoded fails.
*
* @access public
* @param $value
* @param $options
* @param $depth
*
* @return string
* @throws \ErrorException
*/
public static function encodeJson()
{
$args = func_get_args();
// Call json_encode() without the $depth parameter in PHP
// versions less than 5.5.0 as the $depth parameter was added in
// PHP version 5.5.0.
if (version_compare(PHP_VERSION, '5.5.0', '<')) {
$args = array_slice($args, 0, 2);
}
$value = call_user_func_array('json_encode', $args);
if (!(json_last_error() === JSON_ERROR_NONE)) {
if (function_exists('json_last_error_msg')) {
$error_message = 'json_encode error: ' . json_last_error_msg();
} else {
$error_message = 'json_encode error';
}
throw new \ErrorException($error_message);
}
return $value;
}
}
This diff is collapsed.
<?php
namespace Curl;
class StringUtil
{
public static function characterReversePosition($haystack, $needle, $part = false)
{
if (function_exists('\mb_strrchr')) {
return \mb_strrchr($haystack, $needle, $part);
} else {
return \strrchr($haystack, $needle);
}
}
public static function length($string)
{
if (function_exists('\mb_strlen')) {
return \mb_strlen($string);
} else {
return \strlen($string);
}
}
public static function position($haystack, $needle, $offset = 0)
{
if (function_exists('\mb_strpos')) {
return \mb_strpos($haystack, $needle, $offset);
} else {
return \strpos($haystack, $needle, $offset);
}
}
public static function reversePosition($haystack, $needle, $offset = 0)
{
if (function_exists('\mb_strrpos')) {
return \mb_strrpos($haystack, $needle, $offset);
} else {
return \strrpos($haystack, $needle, $offset);
}
}
/**
* Return true when $haystack starts with $needle.
*
* @access public
* @param $haystack
* @param $needle
*
* @return bool
*/
public static function startsWith($haystack, $needle)
{
return self::substring($haystack, 0, self::length($needle)) === $needle;
}
public static function substring($string, $start, $length)
{
if (function_exists('\mb_substr')) {
return \mb_substr($string, $start, $length);
} else {
return \substr($string, $start, $length);
}
}
}
<?php
namespace Curl;
use Curl\StringUtil;
class Url
{
private $baseUrl = null;
private $relativeUrl = null;
public function __construct($base_url, $relative_url = null)
{
$this->baseUrl = $base_url;
$this->relativeUrl = $relative_url;
}
public function __toString()
{
return $this->absolutizeUrl();
}
/**
* Remove dot segments.
*
* Interpret and remove the special "." and ".." path segments from a referenced path.
*/
public static function removeDotSegments($input)
{
// 1. The input buffer is initialized with the now-appended path
// components and the output buffer is initialized to the empty
// string.
$output = '';
// 2. While the input buffer is not empty, loop as follows:
while (!empty($input)) {
// A. If the input buffer begins with a prefix of "../" or "./",
// then remove that prefix from the input buffer; otherwise,
if (StringUtil::startsWith($input, '../')) {
$input = substr($input, 3);
} elseif (StringUtil::startsWith($input, './')) {
$input = substr($input, 2);
// B. if the input buffer begins with a prefix of "/./" or "/.",
// where "." is a complete path segment, then replace that
// prefix with "/" in the input buffer; otherwise,
} elseif (StringUtil::startsWith($input, '/./')) {
$input = substr($input, 2);
} elseif ($input === '/.') {
$input = '/';
// C. if the input buffer begins with a prefix of "/../" or "/..",
// where ".." is a complete path segment, then replace that
// prefix with "/" in the input buffer and remove the last
// segment and its preceding "/" (if any) from the output
// buffer; otherwise,
} elseif (StringUtil::startsWith($input, '/../')) {
$input = substr($input, 3);
$output = substr_replace($output, '', StringUtil::reversePosition($output, '/'));
} elseif ($input === '/..') {
$input = '/';
$output = substr_replace($output, '', StringUtil::reversePosition($output, '/'));
// D. if the input buffer consists only of "." or "..", then remove
// that from the input buffer; otherwise,
} elseif ($input === '.' || $input === '..') {
$input = '';
// E. move the first path segment in the input buffer to the end of
// the output buffer, including the initial "/" character (if
// any) and any subsequent characters up to, but not including,
// the next "/" character or the end of the input buffer.
} elseif (!(($pos = StringUtil::position($input, '/', 1)) === false)) {
$output .= substr($input, 0, $pos);
$input = substr_replace($input, '', 0, $pos);
} else {
$output .= $input;
$input = '';
}
}
// 3. Finally, the output buffer is returned as the result of
// remove_dot_segments.
return $output . $input;
}
/**
* Absolutize url.
*
* Combine the base and relative url into an absolute url.
*/
private function absolutizeUrl()
{
$b = $this->parseUrl($this->baseUrl);
if (!isset($b['path'])) {
$b['path'] = '/';
}
if ($this->relativeUrl === null) {
return $this->unparseUrl($b);
}
$r = $this->parseUrl($this->relativeUrl);
$r['authorized'] = isset($r['scheme']) || isset($r['host']) || isset($r['port'])
|| isset($r['user']) || isset($r['pass']);
$target = array();
if (isset($r['scheme'])) {
$target['scheme'] = $r['scheme'];
$target['host'] = isset($r['host']) ? $r['host'] : null;
$target['port'] = isset($r['port']) ? $r['port'] : null;
$target['user'] = isset($r['user']) ? $r['user'] : null;
$target['pass'] = isset($r['pass']) ? $r['pass'] : null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = isset($r['query']) ? $r['query'] : null;
} else {
$target['scheme'] = isset($b['scheme']) ? $b['scheme'] : null;
if ($r['authorized']) {
$target['host'] = isset($r['host']) ? $r['host'] : null;
$target['port'] = isset($r['port']) ? $r['port'] : null;
$target['user'] = isset($r['user']) ? $r['user'] : null;
$target['pass'] = isset($r['pass']) ? $r['pass'] : null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = isset($r['query']) ? $r['query'] : null;
} else {
$target['host'] = isset($b['host']) ? $b['host'] : null;
$target['port'] = isset($b['port']) ? $b['port'] : null;
$target['user'] = isset($b['user']) ? $b['user'] : null;
$target['pass'] = isset($b['pass']) ? $b['pass'] : null;
if (!isset($r['path']) || $r['path'] === '') {
$target['path'] = $b['path'];
$target['query'] = isset($r['query']) ? $r['query'] : (isset($b['query']) ? $b['query'] : null);
} else {
if (StringUtil::startsWith($r['path'], '/')) {
$target['path'] = self::removeDotSegments($r['path']);
} else {
$base = StringUtil::characterReversePosition($b['path'], '/', true);
if ($base === false) {
$base = '';
}
$target['path'] = self::removeDotSegments($base . '/' . $r['path']);
}
$target['query'] = isset($r['query']) ? $r['query'] : null;
}
}
}
if ($this->relativeUrl === '') {
$target['fragment'] = isset($b['fragment']) ? $b['fragment'] : null;
} else {
$target['fragment'] = isset($r['fragment']) ? $r['fragment'] : null;
}
$absolutized_url = $this->unparseUrl($target);
return $absolutized_url;
}
/**
* Parse url.
*
* Parse url into components of a URI as specified by RFC 3986.
*/
private function parseUrl($url)
{
// ALPHA = A-Z / a-z
$alpha = 'A-Za-z';
// DIGIT = 0-9
$digit = '0-9';
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
$unreserved = $alpha . $digit . preg_quote('-._~');
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
// / "*" / "+" / "," / ";" / "=" / "#"
$sub_delims = preg_quote('!$&\'()*+,;=#');
// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
$hexdig = $digit . 'A-F';
// "The uppercase hexadecimal digits 'A' through 'F' are equivalent to
// the lowercase digits 'a' through 'f', respectively."
$hexdig .= 'a-f';
$pattern = '/(?:[^' . $unreserved . $sub_delims . preg_quote(':@%/?', '/') . ']++|%(?![' . $hexdig . ']{2}))/';
$url = preg_replace_callback(
$pattern,
function ($matches) {
return rawurlencode($matches[0]);
},
$url
);
return parse_url($url);
}
/**
* Unparse url.
*
* Combine url components into a url.
*/
private function unparseUrl($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? $pass . '@' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
$unparsed_url = $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
return $unparsed_url;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment