1
|
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/badges/quality-score.png?b=v2)](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/?branch=v2)
|
2
|
[![Travis CI Build Status](https://travis-ci.org/TYPO3/phar-stream-wrapper.svg?branch=v2)](https://travis-ci.org/TYPO3/phar-stream-wrapper)
|
3
|
|
4
|
# PHP Phar Stream Wrapper
|
5
|
|
6
|
## Abstract & History
|
7
|
|
8
|
Based on Sam Thomas' findings concerning
|
9
|
[insecure deserialization in combination with obfuscation strategies](https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are)
|
10
|
allowing to hide Phar files inside valid image resources, the TYPO3 project
|
11
|
decided back then to introduce a `PharStreamWrapper` to intercept invocations
|
12
|
of the `phar://` stream in PHP and only allow usage for defined locations in
|
13
|
the file system.
|
14
|
|
15
|
Since the TYPO3 mission statement is **inspiring people to share**, we thought
|
16
|
it would be helpful for others to release our `PharStreamWrapper` as standalone
|
17
|
package to the PHP community.
|
18
|
|
19
|
The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas
|
20
|
and has been addressed concerning the specific attack vector and for this generic
|
21
|
`PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th
|
22
|
July 2018.
|
23
|
|
24
|
* https://typo3.org/security/advisory/typo3-core-sa-2018-002/
|
25
|
* https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are
|
26
|
* https://youtu.be/GePBmsNJw6Y
|
27
|
|
28
|
## License
|
29
|
|
30
|
In general the TYPO3 core is released under the GNU General Public License version
|
31
|
2 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and
|
32
|
incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case
|
33
|
you duplicate or modify source code, credits are not required but really appreciated.
|
34
|
|
35
|
## Credits
|
36
|
|
37
|
Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating
|
38
|
back-ports of all sources in order to provide compatibility with PHP v5.3.
|
39
|
|
40
|
## Installation
|
41
|
|
42
|
The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper`
|
43
|
and has minimum requirements of PHP v5.3 ([`v2`](https://github.com/TYPO3/phar-stream-wrapper/tree/v2) branch) and PHP v7.0 ([`master`](https://github.com/TYPO3/phar-stream-wrapper) branch).
|
44
|
|
45
|
### Installation for PHP v7.0
|
46
|
|
47
|
```
|
48
|
composer require typo3/phar-stream-wrapper ^3.0
|
49
|
```
|
50
|
|
51
|
### Installation for PHP v5.3
|
52
|
|
53
|
```
|
54
|
composer require typo3/phar-stream-wrapper ^2.0
|
55
|
```
|
56
|
|
57
|
## Example
|
58
|
|
59
|
The following example is bundled within this package, the shown
|
60
|
`PharExtensionInterceptor` denies all stream wrapper invocations files
|
61
|
not having the `.phar` suffix. Interceptor logic has to be individual and
|
62
|
adjusted to according requirements.
|
63
|
|
64
|
```
|
65
|
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
|
66
|
\TYPO3\PharStreamWrapper\Manager::initialize(
|
67
|
$behavior->withAssertion(new PharExtensionInterceptor())
|
68
|
);
|
69
|
|
70
|
if (in_array('phar', stream_get_wrappers())) {
|
71
|
stream_wrapper_unregister('phar');
|
72
|
stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper');
|
73
|
}
|
74
|
```
|
75
|
|
76
|
* `PharStreamWrapper` defined as class reference will be instantiated each time
|
77
|
`phar://` streams shall be processed.
|
78
|
* `Manager` as singleton pattern being called by `PharStreamWrapper` instances
|
79
|
in order to retrieve individual behavior and settings.
|
80
|
* `Behavior` holds reference to interceptor(s) that shall assert correct/allowed
|
81
|
invocation of a given `$path` for a given `$command`. Interceptors implement
|
82
|
the interface `Assertable`. Interceptors can act individually on following
|
83
|
commands or handle all of them in case not defined specifically:
|
84
|
+ `COMMAND_DIR_OPENDIR`
|
85
|
+ `COMMAND_MKDIR`
|
86
|
+ `COMMAND_RENAME`
|
87
|
+ `COMMAND_RMDIR`
|
88
|
+ `COMMAND_STEAM_METADATA`
|
89
|
+ `COMMAND_STREAM_OPEN`
|
90
|
+ `COMMAND_UNLINK`
|
91
|
+ `COMMAND_URL_STAT`
|
92
|
|
93
|
## Interceptors
|
94
|
|
95
|
The following interceptor is shipped with the package and ready to use in order
|
96
|
to block any Phar invocation of files not having a `.phar` suffix. Besides that
|
97
|
individual interceptors are possible of course.
|
98
|
|
99
|
```
|
100
|
class PharExtensionInterceptor implements Assertable
|
101
|
{
|
102
|
/**
|
103
|
* Determines whether the base file name has a ".phar" suffix.
|
104
|
*
|
105
|
* @param string $path
|
106
|
* @param string $command
|
107
|
* @return bool
|
108
|
* @throws Exception
|
109
|
*/
|
110
|
public function assert($path, $command)
|
111
|
{
|
112
|
if ($this->baseFileContainsPharExtension($path)) {
|
113
|
return true;
|
114
|
}
|
115
|
throw new Exception(
|
116
|
sprintf(
|
117
|
'Unexpected file extension in "%s"',
|
118
|
$path
|
119
|
),
|
120
|
1535198703
|
121
|
);
|
122
|
}
|
123
|
|
124
|
/**
|
125
|
* @param string $path
|
126
|
* @return bool
|
127
|
*/
|
128
|
private function baseFileContainsPharExtension($path)
|
129
|
{
|
130
|
$baseFile = Helper::determineBaseFile($path);
|
131
|
if ($baseFile === null) {
|
132
|
return false;
|
133
|
}
|
134
|
$fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
|
135
|
return strtolower($fileExtension) === 'phar';
|
136
|
}
|
137
|
}
|
138
|
```
|
139
|
|
140
|
### ConjunctionInterceptor
|
141
|
|
142
|
This interceptor combines multiple interceptors implementing `Assertable`.
|
143
|
It succeeds when all nested interceptors succeed as well (logical `AND`).
|
144
|
|
145
|
```
|
146
|
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
|
147
|
\TYPO3\PharStreamWrapper\Manager::initialize(
|
148
|
$behavior->withAssertion(new ConjunctionInterceptor(array(
|
149
|
new PharExtensionInterceptor(),
|
150
|
new PharMetaDataInterceptor()
|
151
|
)))
|
152
|
);
|
153
|
```
|
154
|
|
155
|
### PharExtensionInterceptor
|
156
|
|
157
|
This (basic) interceptor just checks whether the invoked Phar archive has
|
158
|
an according `.phar` file extension. Resolving symbolic links as well as
|
159
|
Phar internal alias resolving are considered as well.
|
160
|
|
161
|
```
|
162
|
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
|
163
|
\TYPO3\PharStreamWrapper\Manager::initialize(
|
164
|
$behavior->withAssertion(new PharExtensionInterceptor())
|
165
|
);
|
166
|
```
|
167
|
|
168
|
### PharMetaDataInterceptor
|
169
|
|
170
|
This interceptor is actually checking serialized Phar meta-data against
|
171
|
PHP objects and would consider a Phar archive malicious in case not only
|
172
|
scalar values are found. A custom low-level `Phar\Reader` is used in order to
|
173
|
avoid using PHP's `Phar` object which would trigger the initial vulnerability.
|
174
|
|
175
|
```
|
176
|
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
|
177
|
\TYPO3\PharStreamWrapper\Manager::initialize(
|
178
|
$behavior->withAssertion(new PharMetaDataInterceptor())
|
179
|
);
|
180
|
```
|
181
|
|
182
|
## Reader
|
183
|
|
184
|
* `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive
|
185
|
* `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive
|
186
|
* `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive
|
187
|
* `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as
|
188
|
documented at http://php.net/manual/en/phar.fileformat.manifestfile.php
|
189
|
* `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub
|
190
|
using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here
|
191
|
* `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest
|
192
|
using `Phar::setAlias('alias.phar')`
|
193
|
* `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data
|
194
|
* `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data
|
195
|
containing only scalar values - in case an object is determined, an according
|
196
|
`Phar\DeserializationException` will be thrown
|
197
|
|
198
|
```
|
199
|
$reader = new Phar\Reader('example.phar');
|
200
|
var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData());
|
201
|
```
|
202
|
|
203
|
## Helper
|
204
|
|
205
|
* `Helper::determineBaseFile(string $path): string`: Determines base file that can be
|
206
|
accessed using the regular file system. For instance the following path
|
207
|
`phar:///home/user/bundle.phar/content.txt` would be resolved to
|
208
|
`/home/user/bundle.phar`.
|
209
|
* `Helper::resetOpCache()`: Resets PHP's OPcache if enabled as work-around for
|
210
|
issues in `include()` or `require()` calls and OPcache delivering wrong
|
211
|
results. More details can be found in PHP's bug tracker, for instance like
|
212
|
https://bugs.php.net/bug.php?id=66569
|
213
|
|
214
|
## Security Contact
|
215
|
|
216
|
In case of finding additional security issues in the TYPO3 project or in this
|
217
|
`PharStreamWrapper` package in particular, please get in touch with the
|
218
|
[TYPO3 Security Team](mailto:security@typo3.org).
|