├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src ├── DisallowedClassesSubstitutor.php └── Unserialize.php /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/ export-ignore 2 | .travis.yml export-ignore 3 | Dockerfile export-ignore 4 | phpunit.xml.dist export-ignore 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | vendor/ 3 | phpunit.xml 4 | composer.lock 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2019 Denis Brumann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Polyfill unserialize [![Build Status](https://travis-ci.org/dbrumann/polyfill-unserialize.svg?branch=master)](https://travis-ci.org/dbrumann/polyfill-unserialize) 2 | === 3 | 4 | Backports unserialize options introduced in PHP 7.0 to older PHP versions. 5 | This was originally designed as a Proof of Concept for Symfony Issue 6 | [#21090](https://github.com/symfony/symfony/pull/21090). 7 | 8 | You can use this package in projects that rely on PHP versions older than 9 | PHP 7.0. In case you are using PHP 7.0+ the original `unserialize()` will be 10 | used instead. 11 | 12 | From the [documentation](https://secure.php.net/manual/en/function.unserialize.php): 13 | 14 | > **Warning** 15 | > 16 | > Do not pass untrusted user input to unserialize() regardless of the options 17 | > value of allowed_classes. Unserialization can result in code being loaded and 18 | > executed due to object instantiation and autoloading, and a malicious user 19 | > may be able to exploit this. Use a safe, standard data interchange format 20 | > such as JSON (via json_decode() and json_encode()) if you need to pass 21 | > serialized data to the user. 22 | 23 | Requirements 24 | ------------ 25 | 26 | - PHP 5.3+ 27 | 28 | Installation 29 | ------------ 30 | 31 | You can install this package via composer: 32 | 33 | ```bash 34 | composer require brumann/polyfill-unserialize "^2.0" 35 | ``` 36 | 37 | Older versions 38 | -------------- 39 | 40 | You can find the most recent 1.x versions in the branch with the same name: 41 | 42 | * [dbrumann/polyfill-unserialize/tree/1.x](https://github.com/dbrumann/polyfill-unserialize/tree/1.x) 43 | 44 | Upgrading 45 | --------- 46 | 47 | Upgrading from 1.x to 2.0 should be seamless and require no changes to code 48 | using the library. There are no changes to the public API, i.e. the names for 49 | classes, methods and arguments as well as argument order and types remain the 50 | same. Version 2.x uses a completely different approach for substituting 51 | disallowed classes, which is why we chose to use a new major release to prevent 52 | issues from unknown side effects in existing installations. 53 | 54 | Known Issues 55 | ------------ 56 | 57 | There is a mismatch in behavior when `allowed_classes` in `$options` is not 58 | of the correct type (array or boolean). PHP 7.0 will not issue a warning that 59 | an invalid type was provided. This library will trigger a warning, similar to 60 | the one PHP 7.1+ will raise and then continue, assuming `false` to make sure 61 | no classes are deserialized by accident. 62 | 63 | Tests 64 | ----- 65 | 66 | You can run the test suite using PHPUnit. It is intentionally not bundled as 67 | dev dependency to make sure this package has the lowest restrictions on the 68 | implementing system as possible. 69 | 70 | Please read the [PHPUnit Manual](https://phpunit.de/manual/current/en/installation.html) 71 | for information how to install it on your system. 72 | 73 | Please make sure to pick a compatible version. If you use PHP 5.6 you should 74 | use PHPUnit 5.7.27 and for older PHP versions you should use PHPUnit 4.8.36. 75 | Older versions of PHPUnit might not support namespaces, meaning they will not 76 | work with the tests. Newer versions only support PHP 7.0+, where this library 77 | is not needed anymore. 78 | 79 | You can run the test suite as follows: 80 | 81 | ```bash 82 | phpunit -c phpunit.xml.dist tests/ 83 | ``` 84 | 85 | Contributing 86 | ------------ 87 | 88 | This package is considered feature complete. As such I will likely not update 89 | it unless there are security issues. 90 | 91 | Should you find any bugs or have questions, feel free to submit an Issue or a 92 | Pull Request on GitHub. 93 | 94 | Development setup 95 | ----------------- 96 | 97 | This library contains a docker setup for development purposes. This allows 98 | running the code on an older PHP version without having to install it locally. 99 | 100 | You can use the setup as follows: 101 | 102 | 1. Go into the project directory 103 | 104 | 1. Build the docker image 105 | 106 | ``` 107 | docker build -t polyfill-unserialize . 108 | ``` 109 | 110 | This will download a debian/jessie container with PHP 5.6 installed. Then 111 | it will download an appropriate version of phpunit for this PHP version. 112 | It will also download composer. It will set the working directory to `/opt/app`. 113 | The resulting image is tagged as `polyfill-unserialize`, which is the name 114 | we will refer to, when running the container. 115 | 116 | 1. You can then run a container based on the image, which will run your tests 117 | 118 | ``` 119 | docker run -it --rm --name polyfill-unserialize-dev -v "$PWD":/opt/app polyfill-unserialize 120 | ``` 121 | 122 | This will run a docker container based on our previously built image. 123 | The container will automatically be removed after phpunit finishes. 124 | We name the image `polyfill-unserialize-dev`. This makes sure only one 125 | instance is running and that we can easily identify a running container by 126 | its name, e.g. in order to remove it manually. 127 | We mount our current directory into the container's working directory. 128 | This ensures that tests run on our current project's state. 129 | 130 | You can repeat the final step as often as you like in order to run the tests. 131 | The output should look something like this: 132 | 133 | ```bash 134 | dbr:polyfill-unserialize/ (improvement/dev_setup*) $ docker run -it --rm --name polyfill-unserialize-dev -v "$PWD":/opt/app polyfill-unserialize 135 | Loading composer repositories with package information 136 | Installing dependencies (including require-dev) from lock file 137 | Nothing to install or update 138 | Generating autoload files 139 | PHPUnit 5.7.27 by Sebastian Bergmann and contributors. 140 | 141 | ...................... 22 / 22 (100%) 142 | 143 | Time: 167 ms, Memory: 13.25MB 144 | 145 | OK (22 tests, 31 assertions) 146 | ``` 147 | 148 | When you are done working on the project you can free up disk space by removing 149 | the initially built image: 150 | 151 | ``` 152 | docker image rm polyfill-unserialize 153 | ``` 154 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brumann/polyfill-unserialize", 3 | "description": "Backports unserialize options introduced in PHP 7.0 to older PHP versions.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Denis Brumann", 9 | "email": "denis.brumann@sensiolabs.de" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Brumann\\Polyfill\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Tests\\Brumann\\Polyfill\\": "tests/" 20 | } 21 | }, 22 | "minimum-stability": "stable", 23 | "require": { 24 | "php": "^5.3|^7.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DisallowedClassesSubstitutor.php: -------------------------------------------------------------------------------- 1 | , ]` and 27 | * marks start and end positions of items to be ignored. 28 | * 29 | * @var array[] 30 | */ 31 | private $ignoreItems = array(); 32 | 33 | /** 34 | * @param string $serialized 35 | * @param string[] $allowedClasses 36 | */ 37 | public function __construct($serialized, array $allowedClasses) 38 | { 39 | $this->serialized = $serialized; 40 | $this->allowedClasses = $allowedClasses; 41 | 42 | $this->buildIgnoreItems(); 43 | $this->substituteObjects(); 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getSubstitutedSerialized() 50 | { 51 | return $this->serialized; 52 | } 53 | 54 | /** 55 | * Identifies items to be ignored - like nested serializations in string literals. 56 | */ 57 | private function buildIgnoreItems() 58 | { 59 | $offset = 0; 60 | while (preg_match(self::PATTERN_STRING, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) { 61 | $length = (int)$matches[1][0]; // given length in serialized data (e.g. `s:123:"` --> 123) 62 | $start = $matches[2][1]; // offset position of quote character 63 | $end = $start + $length + 1; 64 | $offset = $end + 1; 65 | 66 | // serialized string nested in outer serialized string 67 | if ($this->ignore($start, $end)) { 68 | continue; 69 | } 70 | 71 | $this->ignoreItems[] = array($start, $end); 72 | } 73 | } 74 | 75 | /** 76 | * Substitutes disallowed object class names and respects items to be ignored. 77 | */ 78 | private function substituteObjects() 79 | { 80 | $offset = 0; 81 | while (preg_match(self::PATTERN_OBJECT, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) { 82 | $completeMatch = (string)$matches[0][0]; 83 | $completeLength = strlen($completeMatch); 84 | $start = $matches[0][1]; 85 | $end = $start + $completeLength; 86 | $leftBorder = (string)$matches[1][0]; 87 | $className = (string)$matches[2][0]; 88 | $objectSize = (int)$matches[3][0]; 89 | $offset = $end + 1; 90 | 91 | // class name is actually allowed - skip this item 92 | if (in_array($className, $this->allowedClasses, true)) { 93 | continue; 94 | } 95 | // serialized object nested in outer serialized string 96 | if ($this->ignore($start, $end)) { 97 | continue; 98 | } 99 | 100 | $incompleteItem = $this->sanitizeItem($className, $leftBorder, $objectSize); 101 | $incompleteItemLength = strlen($incompleteItem); 102 | $offset = $start + $incompleteItemLength + 1; 103 | 104 | $this->replace($incompleteItem, $start, $end); 105 | $this->shift($end, $incompleteItemLength - $completeLength); 106 | } 107 | } 108 | 109 | /** 110 | * Replaces sanitized object class names in serialized data. 111 | * 112 | * @param string $replacement Sanitized object data 113 | * @param int $start Start offset in serialized data 114 | * @param int $end End offset in serialized data 115 | */ 116 | private function replace($replacement, $start, $end) 117 | { 118 | $this->serialized = substr($this->serialized, 0, $start) 119 | . $replacement . substr($this->serialized, $end); 120 | } 121 | 122 | /** 123 | * Whether given offset positions should be ignored. 124 | * 125 | * @param int $start 126 | * @param int $end 127 | * @return bool 128 | */ 129 | private function ignore($start, $end) 130 | { 131 | foreach ($this->ignoreItems as $ignoreItem) { 132 | if ($ignoreItem[0] <= $start && $ignoreItem[1] >= $end) { 133 | return true; 134 | } 135 | } 136 | 137 | return false; 138 | } 139 | 140 | /** 141 | * Shifts offset positions of ignore items by `$size`. 142 | * This is necessary whenever object class names have been 143 | * substituted which have a different length than before. 144 | * 145 | * @param int $offset 146 | * @param int $size 147 | */ 148 | private function shift($offset, $size) 149 | { 150 | foreach ($this->ignoreItems as &$ignoreItem) { 151 | // only focus on items starting after given offset 152 | if ($ignoreItem[0] < $offset) { 153 | continue; 154 | } 155 | $ignoreItem[0] += $size; 156 | $ignoreItem[1] += $size; 157 | } 158 | } 159 | 160 | /** 161 | * Sanitizes object class item. 162 | * 163 | * @param string $className 164 | * @param int $leftBorder 165 | * @param int $objectSize 166 | * @return string 167 | */ 168 | private function sanitizeItem($className, $leftBorder, $objectSize) 169 | { 170 | return sprintf( 171 | '%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s', 172 | $leftBorder, 173 | $objectSize + 1, // size of object + 1 for added string 174 | \serialize($className) 175 | ); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Unserialize.php: -------------------------------------------------------------------------------- 1 | = 70000) { 18 | return \unserialize($serialized, $options); 19 | } 20 | if (!array_key_exists('allowed_classes', $options) || true === $options['allowed_classes']) { 21 | return \unserialize($serialized); 22 | } 23 | $allowedClasses = $options['allowed_classes']; 24 | if (false === $allowedClasses) { 25 | $allowedClasses = array(); 26 | } 27 | if (!is_array($allowedClasses)) { 28 | $allowedClasses = array(); 29 | trigger_error( 30 | 'unserialize(): allowed_classes option should be array or boolean', 31 | E_USER_WARNING 32 | ); 33 | } 34 | 35 | $worker = new DisallowedClassesSubstitutor($serialized, $allowedClasses); 36 | 37 | return \unserialize($worker->getSubstitutedSerialized()); 38 | } 39 | } 40 | --------------------------------------------------------------------------------