Skip to content

Commit d8d246c

Browse files
committed
SA-CORE-2019-002 by greggles, cashwilliams, EclipseGc, larowlan, samuel.mortenson, alexpott, tedbow, effulgentsia, Fabianx, xjm, mlhess
1 parent c4996d2 commit d8d246c

File tree

7 files changed

+159
-1
lines changed

7 files changed

+159
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"symfony/process": "~3.4.0",
3232
"symfony/polyfill-iconv": "^1.0",
3333
"symfony/yaml": "~3.4.5",
34+
"typo3/phar-stream-wrapper": "^2.0.1",
3435
"twig/twig": "^1.35.0",
3536
"doctrine/common": "^2.5",
3637
"doctrine/annotations": "^1.2",

lib/Drupal/Core/DrupalKernel.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Drupal\Core\Http\TrustedHostsRequestFactory;
2020
use Drupal\Core\Installer\InstallerRedirectTrait;
2121
use Drupal\Core\Language\Language;
22+
use Drupal\Core\Security\PharExtensionInterceptor;
2223
use Drupal\Core\Security\RequestSanitizer;
2324
use Drupal\Core\Site\Settings;
2425
use Drupal\Core\Test\TestDatabase;
@@ -35,6 +36,9 @@
3536
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
3637
use Symfony\Component\HttpKernel\TerminableInterface;
3738
use Symfony\Component\Routing\Route;
39+
use TYPO3\PharStreamWrapper\Manager as PharStreamWrapperManager;
40+
use TYPO3\PharStreamWrapper\Behavior as PharStreamWrapperBehavior;
41+
use TYPO3\PharStreamWrapper\PharStreamWrapper;
3842

3943
/**
4044
* The DrupalKernel class is the core of Drupal itself.
@@ -471,6 +475,26 @@ public function boot() {
471475
// Initialize the container.
472476
$this->initializeContainer();
473477

478+
if (in_array('phar', stream_get_wrappers(), TRUE)) {
479+
// Set up a stream wrapper to handle insecurities due to PHP's builtin
480+
// phar stream wrapper. This is not registered as a regular stream wrapper
481+
// to prevent \Drupal\Core\File\FileSystem::validScheme() treating "phar"
482+
// as a valid scheme.
483+
try {
484+
$behavior = new PharStreamWrapperBehavior();
485+
PharStreamWrapperManager::initialize(
486+
$behavior->withAssertion(new PharExtensionInterceptor())
487+
);
488+
}
489+
catch (\LogicException $e) {
490+
// Continue if the PharStreamWrapperManager is already initialized. For
491+
// example, this occurs during a module install.
492+
// @see \Drupal\Core\Extension\ModuleInstaller::install()
493+
};
494+
stream_wrapper_unregister('phar');
495+
stream_wrapper_register('phar', PharStreamWrapper::class);
496+
}
497+
474498
$this->booted = TRUE;
475499

476500
return $this;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace Drupal\Core\Security;
4+
5+
use TYPO3\PharStreamWrapper\Assertable;
6+
use TYPO3\PharStreamWrapper\Helper;
7+
use TYPO3\PharStreamWrapper\Exception;
8+
9+
/**
10+
* An alternate PharExtensionInterceptor to support phar-based CLI tools.
11+
*
12+
* @see \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor
13+
*/
14+
class PharExtensionInterceptor implements Assertable {
15+
16+
/**
17+
* Determines whether phar file is allowed to execute.
18+
*
19+
* The phar file is allowed to execute if:
20+
* - the base file name has a ".phar" suffix.
21+
* - it is the CLI tool that has invoked the interceptor.
22+
*
23+
* @param string $path
24+
* The path of the phar file to check.
25+
*
26+
* @param string $command
27+
* The command being carried out.
28+
*
29+
* @return bool
30+
* TRUE if the phar file is allowed to execute.
31+
*
32+
* @throws Exception
33+
* Thrown when the file is not allowed to execute.
34+
*/
35+
public function assert($path, $command) {
36+
if ($this->baseFileContainsPharExtension($path)) {
37+
return TRUE;
38+
}
39+
throw new Exception(
40+
sprintf(
41+
'Unexpected file extension in "%s"',
42+
$path
43+
),
44+
1535198703
45+
);
46+
}
47+
48+
/**
49+
* @param string $path
50+
* The path of the phar file to check.
51+
*
52+
* @return bool
53+
* TRUE if the file has a .phar extension or if the execution has been
54+
* invoked by the phar file.
55+
*/
56+
private function baseFileContainsPharExtension($path) {
57+
$baseFile = Helper::determineBaseFile($path);
58+
if ($baseFile === NULL) {
59+
return FALSE;
60+
}
61+
// If the stream wrapper is registered by invoking a phar file that does
62+
// not not have .phar extension then this should be allowed. For
63+
// example, some CLI tools recommend removing the extension.
64+
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
65+
$caller = array_pop($backtrace);
66+
if (isset($caller['file']) && $baseFile === $caller['file']) {
67+
return TRUE;
68+
}
69+
$fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
70+
return strtolower($fileExtension) === 'phar';
71+
}
72+
73+
}

modules/file/file.module

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use Drupal\Core\Template\Attribute;
2323
/**
2424
* The regex pattern used when checking for insecure file types.
2525
*/
26-
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(php|pl|py|cgi|asp|js)(\.|$)/i');
26+
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');
2727

2828
// Load all Field module hooks for File.
2929
require_once __DIR__ . '/file.field.inc';
6.75 KB
Binary file not shown.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Drupal\KernelTests\Core\File;
4+
5+
use Drupal\KernelTests\KernelTestBase;
6+
7+
/**
8+
* Tests that the phar stream wrapper works.
9+
*
10+
* @group File
11+
*/
12+
class PharWrapperTest extends KernelTestBase {
13+
14+
/**
15+
* Tests that only valid phar files can be used.
16+
*/
17+
public function testPharFile() {
18+
$base = $this->getDrupalRoot() . '/core/modules/simpletest/files';
19+
// Ensure that file operations via the phar:// stream wrapper work for phar
20+
// files with the .phar extension.
21+
$this->assertFalse(file_exists("phar://$base/phar-1.phar/no-such-file.php"));
22+
$this->assertTrue(file_exists("phar://$base/phar-1.phar/index.php"));
23+
$file_contents = file_get_contents("phar://$base/phar-1.phar/index.php");
24+
$expected_hash = 'c7e7904ea573c5ebea3ef00bb08c1f86af1a45961fbfbeb1892ff4a98fd73ad5';
25+
$this->assertSame($expected_hash, hash('sha256', $file_contents));
26+
27+
// Ensure that file operations via the phar:// stream wrapper throw an
28+
// exception for files without the .phar extension.
29+
$this->setExpectedException('TYPO3\PharStreamWrapper\Exception');
30+
file_exists("phar://$base/image-2.jpg/index.php");
31+
}
32+
33+
}

tests/Drupal/KernelTests/Core/File/StreamWrapperTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,31 @@ public function testGetValidStreamScheme() {
144144
$this->assertFalse(file_stream_wrapper_valid_scheme(file_uri_scheme('foo://asdf')), 'Did not get a valid stream scheme from foo://asdf');
145145
}
146146

147+
/**
148+
* Tests that phar stream wrapper is registered as expected.
149+
*
150+
* @see \Drupal\Core\StreamWrapper\StreamWrapperManager::register()
151+
*/
152+
public function testPharStreamWrapperRegistration() {
153+
if (!in_array('phar', stream_get_wrappers(), TRUE)) {
154+
$this->markTestSkipped('There is no phar stream wrapper registered. PHP is probably compiled without phar support.');
155+
}
156+
// Ensure that phar is not treated as a valid scheme.
157+
$stream_wrapper_manager = $this->container->get('stream_wrapper_manager');
158+
$this->assertFalse($stream_wrapper_manager->getViaScheme('phar'));
159+
160+
// Ensure that calling register again and unregister do not create errors
161+
// due to the PharStreamWrapperManager singleton.
162+
$stream_wrapper_manager->register();
163+
$this->assertContains('public', stream_get_wrappers());
164+
$this->assertContains('phar', stream_get_wrappers());
165+
$stream_wrapper_manager->unregister();
166+
$this->assertNotContains('public', stream_get_wrappers());
167+
// This will have reverted to the builtin phar stream wrapper.
168+
$this->assertContains('phar', stream_get_wrappers());
169+
$stream_wrapper_manager->register();
170+
$this->assertContains('public', stream_get_wrappers());
171+
$this->assertContains('phar', stream_get_wrappers());
172+
}
173+
147174
}

0 commit comments

Comments
 (0)