diff --git a/typo3/sysext/core/Classes/Core/Bootstrap.php b/typo3/sysext/core/Classes/Core/Bootstrap.php
index 80a62ffefcaf5d7884a1ec591f7de1dfd0ba1945..8823933c47c5f5ea95573f7e002a57a52afdf9f0 100644
--- a/typo3/sysext/core/Classes/Core/Bootstrap.php
+++ b/typo3/sysext/core/Classes/Core/Bootstrap.php
@@ -22,6 +22,7 @@ use Psr\Container\NotFoundExceptionInterface;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\IO\PharStreamWrapper;
 use TYPO3\CMS\Core\Localization\Locales;
 use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Package\FailsafePackageManager;
@@ -87,6 +88,7 @@ class Bootstrap
         }
         static::populateLocalConfiguration($configurationManager);
         static::initializeErrorHandling();
+        static::initializeIO();
 
         $logManager = new LogManager($requestId);
         $cacheManager = static::createCacheManager($failsafe ? true : false);
@@ -678,6 +680,17 @@ class Bootstrap
         }
     }
 
+    /**
+     * Initializes IO and stream wrapper related behavior.
+     */
+    protected static function initializeIO()
+    {
+        if (in_array('phar', stream_get_wrappers())) {
+            stream_wrapper_unregister('phar');
+            stream_wrapper_register('phar', PharStreamWrapper::class);
+        }
+    }
+
     /**
      * Set PHP memory limit depending on value of
      * $GLOBALS['TYPO3_CONF_VARS']['SYS']['setMemoryLimit']
diff --git a/typo3/sysext/core/Classes/IO/PharStreamWrapper.php b/typo3/sysext/core/Classes/IO/PharStreamWrapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b53bdbcb6fbfdc5e3d68caa02c636a396636b9b
--- /dev/null
+++ b/typo3/sysext/core/Classes/IO/PharStreamWrapper.php
@@ -0,0 +1,557 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\IO;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+
+class PharStreamWrapper
+{
+    /**
+     * Internal stream constants that are not exposed to PHP, but used...
+     * @see https://github.com/php/php-src/blob/e17fc0d73c611ad0207cac8a4a01ded38251a7dc/main/php_streams.h
+     */
+    protected const STREAM_OPEN_FOR_INCLUDE = 128;
+
+    /**
+     * @var resource
+     */
+    public $context;
+
+    /**
+     * @var resource
+     */
+    protected $internalResource;
+
+    /**
+     * @return bool
+     */
+    public function dir_closedir(): bool
+    {
+        if (!is_resource($this->internalResource)) {
+            return false;
+        }
+
+        $this->invokeInternalStreamWrapper(
+            'closedir',
+            $this->internalResource
+        );
+        return !is_resource($this->internalResource);
+    }
+
+    /**
+     * @param string $path
+     * @param int $options
+     * @return bool
+     */
+    public function dir_opendir(string $path, int $options): bool
+    {
+        $this->assertPath($path);
+        $this->internalResource = $this->invokeInternalStreamWrapper(
+            'opendir',
+            $path,
+            $this->context
+        );
+        return is_resource($this->internalResource);
+    }
+
+    /**
+     * @return string|false
+     */
+    public function dir_readdir()
+    {
+        return $this->invokeInternalStreamWrapper(
+            'readdir',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function dir_rewinddir(): bool
+    {
+        if (!is_resource($this->internalResource)) {
+            return false;
+        }
+
+        $this->invokeInternalStreamWrapper(
+            'rewinddir',
+            $this->internalResource
+        );
+        return is_resource($this->internalResource);
+    }
+
+    /**
+     * @param string $path
+     * @param int $mode
+     * @param int $options
+     * @return bool
+     */
+    public function mkdir(string $path, int $mode, int $options): bool
+    {
+        $this->assertPath($path);
+        return $this->invokeInternalStreamWrapper(
+            'mkdir',
+            $path,
+            $mode,
+            (bool)($options & STREAM_MKDIR_RECURSIVE),
+            $this->context
+        );
+    }
+
+    /**
+     * @param string $path_from
+     * @param string $path_to
+     * @return bool
+     */
+    public function rename(string $path_from, string $path_to): bool
+    {
+        $this->assertPath($path_from);
+        $this->assertPath($path_to);
+        return $this->invokeInternalStreamWrapper(
+            'rename',
+            $path_from,
+            $path_to,
+            $this->context
+        );
+    }
+
+    public function rmdir(string $path, int $options): bool
+    {
+        $this->assertPath($path);
+        return $this->invokeInternalStreamWrapper(
+            'rmdir',
+            $path,
+            $this->context
+        );
+    }
+
+    /**
+     * @param int $cast_as
+     */
+    public function stream_cast(int $cast_as)
+    {
+        throw new PharStreamWrapperException(
+            'Method stream_select() cannot be used',
+            1530103999
+        );
+    }
+
+    public function stream_close()
+    {
+        $this->invokeInternalStreamWrapper(
+            'fclose',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function stream_eof(): bool
+    {
+        return $this->invokeInternalStreamWrapper(
+            'feof',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return bool
+     */
+    public function stream_flush(): bool
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fflush',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @param int $operation
+     * @return bool
+     */
+    public function stream_lock(int $operation): bool
+    {
+        return $this->invokeInternalStreamWrapper(
+            'flock',
+            $this->internalResource,
+            $operation
+        );
+    }
+
+    /**
+     * @param string $path
+     * @param int $option
+     * @param string|int $value
+     * @return bool
+     */
+    public function stream_metadata(string $path, int $option, $value): bool
+    {
+        $this->assertPath($path);
+        if ($option === STREAM_META_TOUCH) {
+            return $this->invokeInternalStreamWrapper(
+                'touch',
+                $path,
+                ...$value
+            );
+        }
+        if ($option === STREAM_META_OWNER_NAME || $option === STREAM_META_OWNER) {
+            return $this->invokeInternalStreamWrapper(
+                'chown',
+                $path,
+                $value
+            );
+        }
+        if ($option === STREAM_META_GROUP_NAME || $option === STREAM_META_GROUP) {
+            return $this->invokeInternalStreamWrapper(
+                'chgrp',
+                $path,
+                $value
+            );
+        }
+        if ($option === STREAM_META_ACCESS) {
+            return $this->invokeInternalStreamWrapper(
+                'chmod',
+                $path,
+                $value
+            );
+        }
+        return false;
+    }
+
+    /**
+     * @param string $path
+     * @param string $mode
+     * @param int $options
+     * @param string|null $opened_path
+     * @return bool
+     */
+    public function stream_open(
+        string $path,
+        string $mode,
+        int $options,
+        string &$opened_path = null
+    ): bool {
+        $this->assertPath($path);
+        $arguments = [$path, $mode, (bool)($options & STREAM_USE_PATH)];
+        // only add stream context for non include/require calls
+        if (!($options & static::STREAM_OPEN_FOR_INCLUDE)) {
+            $arguments[] = $this->context;
+        // work around https://bugs.php.net/bug.php?id=66569
+        // for including files from Phar stream with OPcache enabled
+        } else {
+            $this->resetOpCache();
+        }
+        $this->internalResource = $this->invokeInternalStreamWrapper(
+            'fopen',
+            ...$arguments
+        );
+        if (!is_resource($this->internalResource)) {
+            return false;
+        }
+        if ($opened_path !== null) {
+            $metaData = stream_get_meta_data($this->internalResource);
+            $opened_path = $metaData['uri'];
+        }
+        return true;
+    }
+
+    /**
+     * @param int $count
+     * @return string
+     */
+    public function stream_read(int $count): string
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fread',
+            $this->internalResource,
+            $count
+        );
+    }
+
+    /**
+     * @param int $offset
+     * @param int $whence
+     * @return bool
+     */
+    public function stream_seek(int $offset, int $whence = SEEK_SET): bool
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fseek',
+            $this->internalResource,
+            $offset,
+            $whence
+        ) !== -1;
+    }
+
+    /**
+     * @param int $option
+     * @param int $arg1
+     * @param int $arg2
+     * @return bool
+     */
+    public function stream_set_option(int $option, int $arg1, int $arg2): bool
+    {
+        if ($option === STREAM_OPTION_BLOCKING) {
+            return $this->invokeInternalStreamWrapper(
+                'stream_set_blocking',
+                $this->internalResource,
+                $arg1
+            );
+        }
+        if ($option === STREAM_OPTION_READ_TIMEOUT) {
+            return $this->invokeInternalStreamWrapper(
+                'stream_set_timeout',
+                $this->internalResource,
+                $arg1,
+                $arg2
+            );
+        }
+        if ($option === STREAM_OPTION_WRITE_BUFFER) {
+            return $this->invokeInternalStreamWrapper(
+                'stream_set_write_buffer',
+                $this->internalResource,
+                $arg2
+            ) === 0;
+        }
+        return false;
+    }
+
+    /**
+     * @return array
+     */
+    public function stream_stat(): array
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fstat',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @return int
+     */
+    public function stream_tell(): int
+    {
+        return $this->invokeInternalStreamWrapper(
+            'ftell',
+            $this->internalResource
+        );
+    }
+
+    /**
+     * @param int $new_size
+     * @return bool
+     */
+    public function stream_truncate(int $new_size): bool
+    {
+        return $this->invokeInternalStreamWrapper(
+            'ftruncate',
+            $this->internalResource,
+            $new_size
+        );
+    }
+
+    /**
+     * @param string $data
+     * @return int
+     */
+    public function stream_write(string $data): int
+    {
+        return $this->invokeInternalStreamWrapper(
+            'fwrite',
+            $this->internalResource,
+            $data
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    public function unlink(string $path): bool
+    {
+        $this->assertPath($path);
+        return $this->invokeInternalStreamWrapper(
+            'unlink',
+            $path,
+            $this->context
+        );
+    }
+
+    /**
+     * @param string $path
+     * @param int $flags
+     * @return array|false
+     */
+    public function url_stat(string $path, int $flags)
+    {
+        $this->assertPath($path);
+        $functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat';
+        return $this->invokeInternalStreamWrapper($functionName, $path);
+    }
+
+    /**
+     * @param string $path
+     * @return bool
+     */
+    protected function isAllowed(string $path): bool
+    {
+        $path = $this->determineBaseFile($path);
+        if (!GeneralUtility::isAbsPath($path)) {
+            $path = Environment::getPublicPath() . '/' . $path;
+        }
+
+        if (GeneralUtility::validPathStr($path)
+            && GeneralUtility::isFirstPartOfStr(
+                $path,
+                Environment::getPublicPath() . '/typo3conf/ext/'
+            )
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Normalizes a path, removes phar:// prefix, fixes Windows directory
+     * separators. Result is without trailing slash.
+     *
+     * @param string $path
+     * @return string
+     */
+    protected function normalizePath(string $path): string
+    {
+        return rtrim(
+            PathUtility::getCanonicalPath(
+                GeneralUtility::fixWindowsFilePath(
+                    $this->removePharPrefix($path)
+                )
+            ),
+            '/'
+        );
+    }
+
+    /**
+     * @param string $path
+     * @return string
+     */
+    protected function removePharPrefix(string $path): string
+    {
+        return preg_replace('#^phar://#i', '', $path);
+    }
+
+    /**
+     * Determines base file that can be accessed using the regular file system.
+     * For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
+     * into "/home/user/bundle.phar".
+     *
+     * @param string $path
+     * @return string|null
+     */
+    protected function determineBaseFile(string $path)
+    {
+        $parts = explode('/', $this->normalizePath($path));
+
+        while (count($parts)) {
+            $currentPath = implode('/', $parts);
+            if (file_exists($currentPath)) {
+                return $currentPath;
+            }
+            array_pop($parts);
+        }
+
+        return null;
+    }
+
+    /**
+     * Determines whether the requested path is the base file.
+     *
+     * @param string $path
+     * @return bool
+     * @deprecated Currently not used
+     */
+    protected function isBaseFile(string $path): bool
+    {
+        $path = $this->normalizePath($path);
+        $baseFile = $this->determineBaseFile($path);
+        return $path === $baseFile;
+    }
+
+    /**
+     * Asserts the given path to a Phar file.
+     *
+     * @param string $path
+     * @throws PharStreamWrapperException
+     */
+    protected function assertPath(string $path)
+    {
+        if (!$this->isAllowed($path)) {
+            throw new PharStreamWrapperException(
+                sprintf('Executing  %s is denied', $path),
+                1530103998
+            );
+        }
+    }
+
+    protected function resetOpCache()
+    {
+        if (function_exists('opcache_reset')
+            && function_exists('opcache_get_status')
+            && !empty(opcache_get_status()['opcache_enabled'])
+        ) {
+            opcache_reset();
+        }
+    }
+
+    /**
+     * Invokes commands on the native PHP Phar stream wrapper.
+     *
+     * @param string $functionName
+     * @param mixed ...$arguments
+     * @return mixed
+     */
+    protected function invokeInternalStreamWrapper(string $functionName, ...$arguments)
+    {
+        $silentExecution = $functionName{0} === '@';
+        $functionName = ltrim($functionName, '@');
+        $this->restoreInternalSteamWrapper();
+
+        if ($silentExecution) {
+            $result = @call_user_func_array($functionName, $arguments);
+        } else {
+            $result = call_user_func_array($functionName, $arguments);
+        }
+
+        $this->registerStreamWrapper();
+        return $result;
+    }
+
+    protected function restoreInternalSteamWrapper()
+    {
+        stream_wrapper_restore('phar');
+    }
+
+    protected function registerStreamWrapper()
+    {
+        stream_wrapper_unregister('phar');
+        stream_wrapper_register('phar', static::class);
+    }
+}
diff --git a/typo3/sysext/core/Classes/IO/PharStreamWrapperException.php b/typo3/sysext/core/Classes/IO/PharStreamWrapperException.php
new file mode 100644
index 0000000000000000000000000000000000000000..57126c121d32f173773a2a642fd13e96e54175a6
--- /dev/null
+++ b/typo3/sysext/core/Classes/IO/PharStreamWrapperException.php
@@ -0,0 +1,20 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\IO;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class PharStreamWrapperException extends \RuntimeException
+{
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/bundle.phar b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/bundle.phar
new file mode 100644
index 0000000000000000000000000000000000000000..c829673de8a90874a87f9564e7b35e9c149d79d6
Binary files /dev/null and b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/bundle.phar differ
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/ext_emconf.php
new file mode 100644
index 0000000000000000000000000000000000000000..08b3e193cefdcbe492e996db17623fc322130364
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/ext_emconf.php
@@ -0,0 +1,21 @@
+<?php
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'Test Resources',
+    'description' => 'Test Resources',
+    'category' => 'example',
+    'version' => '9.4.0',
+    'state' => 'beta',
+    'uploadfolder' => 0,
+    'createDirs' => '',
+    'clearCacheOnLoad' => 0,
+    'author' => 'Oliver Hader',
+    'author_email' => 'oliver@typo3.org',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '9.4.0'
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperTest.php b/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c8866959b468b797aad74d53447f5d8627ee1ad3
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/IO/PharStreamWrapperTest.php
@@ -0,0 +1,402 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Functional\IO;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\IO\PharStreamWrapper;
+use TYPO3\CMS\Core\IO\PharStreamWrapperException;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class PharStreamWrapperTest extends FunctionalTestCase
+{
+    /**
+     * @var array
+     */
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources'
+    ];
+
+    /**
+     * @var array
+     */
+    protected $pathsToLinkInTestInstance = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_resources/bundle.phar' => 'fileadmin/bundle.phar'
+    ];
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        if (!in_array('phar', stream_get_wrappers())) {
+            $this->markTestSkipped('Phar stream wrapper is not registered');
+        }
+
+        stream_wrapper_unregister('phar');
+        stream_wrapper_register('phar', PharStreamWrapper::class);
+    }
+
+    protected function tearDown()
+    {
+        stream_wrapper_restore('phar');
+        parent::tearDown();
+    }
+
+    public function directoryActionAllowsInvocationDataProvider()
+    {
+        $allowedPath = 'typo3conf/ext/test_resources/bundle.phar';
+
+        return [
+            'root directory' => [
+                $allowedPath,
+                ['Classes', 'Resources']
+            ],
+            'Classes/Domain/Model directory' => [
+                $allowedPath . '/Classes/Domain/Model',
+                ['DemoModel.php']
+            ],
+            'Resources directory' => [
+                $allowedPath . '/Resources',
+                ['content.txt']
+            ],
+        ];
+    }
+
+    /**
+     * @param string $path
+     *
+     * @test
+     * @dataProvider directoryActionAllowsInvocationDataProvider
+     */
+    public function directoryOpenAllowsInvocation(string $path)
+    {
+        $path = $this->instancePath . '/' . $path;
+        $handle = opendir('phar://' . $path);
+        self::assertInternalType('resource', $handle);
+    }
+
+    /**
+     * @param string $path
+     * @param $expectation
+     *
+     * @test
+     * @dataProvider directoryActionAllowsInvocationDataProvider
+     */
+    public function directoryReadAllowsInvocation(string $path, array $expectation)
+    {
+        $path = $this->instancePath . '/' . $path;
+
+        $items = [];
+        $handle = opendir('phar://' . $path);
+        while (false !== $item = readdir($handle)) {
+            $items[] = $item;
+        }
+
+        self::assertSame($expectation, $items);
+    }
+
+    /**
+     * @param string $path
+     * @param $expectation
+     *
+     * @test
+     * @dataProvider directoryActionAllowsInvocationDataProvider
+     */
+    public function directoryCloseAllowsInvocation(string $path, array $expectation)
+    {
+        $path = $this->instancePath . '/' . $path;
+
+        $handle = opendir('phar://' . $path);
+        closedir($handle);
+
+        self::assertFalse(is_resource($handle));
+    }
+
+    public function directoryActionDeniesInvocationDataProvider()
+    {
+        $deniedPath = 'fileadmin/bundle.phar';
+
+        return [
+            'root directory' => [
+                $deniedPath,
+                ['Classes', 'Resources']
+            ],
+            'Classes/Domain/Model directory' => [
+                $deniedPath . '/Classes/Domain/Model',
+                ['DemoModel.php']
+            ],
+            'Resources directory' => [
+                $deniedPath . '/Resources',
+                ['content.txt']
+            ],
+        ];
+    }
+
+    /**
+     * @param string $path
+     *
+     * @test
+     * @dataProvider directoryActionDeniesInvocationDataProvider
+     */
+    public function directoryActionDeniesInvocation(string $path)
+    {
+        self::expectException(PharStreamWrapperException::class);
+        self::expectExceptionCode(1530103998);
+
+        $path = $this->instancePath . '/' . $path;
+        opendir('phar://' . $path);
+    }
+
+    /**
+     * @return array
+     */
+    public function urlStatAllowsInvocationDataProvider(): array
+    {
+        $allowedPath = 'typo3conf/ext/test_resources/bundle.phar';
+
+        return [
+            'filesize base file' => [
+                'filesize',
+                $allowedPath,
+                0, // Phar base file always has zero size when accessed through phar://
+            ],
+            'filesize Resources/content.txt' => [
+                'filesize',
+                $allowedPath . '/Resources/content.txt',
+                21,
+            ],
+            'is_file base file' => [
+                'is_file',
+                $allowedPath,
+                false, // Phar base file is not a file when accessed through phar://
+            ],
+            'is_file Resources/content.txt' => [
+                'is_file',
+                $allowedPath . '/Resources/content.txt',
+                true,
+            ],
+            'is_dir base file' => [
+                'is_dir',
+                $allowedPath,
+                true, // Phar base file is a directory when accessed through phar://
+            ],
+            'is_dir Resources/content.txt' => [
+                'is_dir',
+                $allowedPath . '/Resources/content.txt',
+                false,
+            ],
+            'file_exists base file' => [
+                'file_exists',
+                $allowedPath,
+                true
+            ],
+            'file_exists Resources/content.txt' => [
+                'file_exists',
+                $allowedPath . '/Resources/content.txt',
+                true
+            ],
+        ];
+    }
+
+    /**
+     * @param string $functionName
+     * @param string $path
+     * @param mixed $expectation
+     *
+     * @test
+     * @dataProvider urlStatAllowsInvocationDataProvider
+     */
+    public function urlStatAllowsInvocation(string $functionName, string $path, $expectation)
+    {
+        $path = $this->instancePath . '/' . $path;
+
+        self::assertSame(
+            $expectation,
+            call_user_func($functionName, 'phar://' . $path)
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function urlStatDeniesInvocationDataProvider(): array
+    {
+        $deniedPath = 'fileadmin/bundle.phar';
+
+        return [
+            'filesize base file' => [
+                'filesize',
+                $deniedPath,
+                0, // Phar base file always has zero size when accessed through phar://
+            ],
+            'filesize Resources/content.txt' => [
+                'filesize',
+                $deniedPath . '/Resources/content.txt',
+                21,
+            ],
+            'is_file base file' => [
+                'is_file',
+                $deniedPath,
+                false, // Phar base file is not a file when accessed through phar://
+            ],
+            'is_file Resources/content.txt' => [
+                'is_file',
+                $deniedPath . '/Resources/content.txt',
+                true,
+            ],
+            'is_dir base file' => [
+                'is_dir',
+                $deniedPath,
+                true, // Phar base file is a directory when accessed through phar://
+            ],
+            'is_dir Resources/content.txt' => [
+                'is_dir',
+                $deniedPath . '/Resources/content.txt',
+                false,
+            ],
+            'file_exists base file' => [
+                'file_exists',
+                $deniedPath,
+                true
+            ],
+            'file_exists Resources/content.txt' => [
+                'file_exists',
+                $deniedPath . '/Resources/content.txt',
+                true
+            ],
+        ];
+    }
+
+    /**
+     * @param string $functionName
+     * @param string $path
+     * @param mixed $expectation
+     *
+     * @test
+     * @dataProvider urlStatDeniesInvocationDataProvider
+     */
+    public function urlStatDeniesInvocation(string $functionName, string $path)
+    {
+        self::expectException(PharStreamWrapperException::class);
+        self::expectExceptionCode(1530103998);
+
+        $path = $this->instancePath . '/' . $path;
+        call_user_func($functionName, 'phar://' . $path);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileOpen()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        self::assertInternalType('resource', $handle);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileRead()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        $content = fread($handle, 1024);
+        self::assertSame('TYPO3 demo text file.', $content);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileEnd()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        fread($handle, 1024);
+        self::assertTrue(feof($handle));
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileClose()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $handle = fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+        fclose($handle);
+        self::assertFalse(is_resource($handle));
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForFileGetContents()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        $content = file_get_contents('phar://' . $allowedPath . '/Resources/content.txt');
+        self::assertSame('TYPO3 demo text file.', $content);
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenAllowsInvocationForInclude()
+    {
+        $allowedPath = $this->instancePath . '/typo3conf/ext/test_resources/bundle.phar';
+        include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php');
+
+        self::assertTrue(
+            class_exists(
+                \TYPO3Demo\Demo\Domain\Model\DemoModel::class,
+                false
+            )
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenDeniesInvocationForFileOpen()
+    {
+        self::expectException(PharStreamWrapperException::class);
+        self::expectExceptionCode(1530103998);
+
+        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
+        fopen('phar://' . $allowedPath . '/Resources/content.txt', 'r');
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenDeniesInvocationForFileGetContents()
+    {
+        self::expectException(PharStreamWrapperException::class);
+        self::expectExceptionCode(1530103998);
+
+        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
+        file_get_contents('phar://' . $allowedPath . '/Resources/content.txt');
+    }
+
+    /**
+     * @test
+     */
+    public function streamOpenDeniesInvocationForInclude()
+    {
+        self::expectException(PharStreamWrapperException::class);
+        self::expectExceptionCode(1530103998);
+
+        $allowedPath = $this->instancePath . '/fileadmin/bundle.phar';
+        include('phar://' . $allowedPath . '/Classes/Domain/Model/DemoModel.php');
+    }
+}