Skip to content

Commit a270b5b

Browse files
MAGETWO-98677: Path check for images
1 parent 79de888 commit a270b5b

File tree

7 files changed

+247
-23
lines changed

7 files changed

+247
-23
lines changed

app/code/Magento/Cms/Helper/Wysiwyg/Images.php

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ class Images extends \Magento\Framework\App\Helper\AbstractHelper
2727
protected $_currentUrl;
2828

2929
/**
30-
* Currenty selected store ID if applicable
30+
* Currently selected store ID if applicable
3131
*
3232
* @var int
3333
*/
34-
protected $_storeId = null;
34+
protected $_storeId;
3535

3636
/**
3737
* @var \Magento\Framework\Filesystem\Directory\Write
@@ -71,7 +71,7 @@ public function __construct(
7171
$this->_storeManager = $storeManager;
7272

7373
$this->_directory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
74-
$this->_directory->create(\Magento\Cms\Model\Wysiwyg\Config::IMAGE_DIRECTORY);
74+
$this->_directory->create($this->getStorageRoot());
7575
}
7676

7777
/**
@@ -93,7 +93,17 @@ public function setStoreId($store)
9393
*/
9494
public function getStorageRoot()
9595
{
96-
return $this->_directory->getAbsolutePath(\Magento\Cms\Model\Wysiwyg\Config::IMAGE_DIRECTORY);
96+
return $this->_directory->getAbsolutePath($this->getStorageRootSubpath());
97+
}
98+
99+
/**
100+
* Get image storage root subpath. User is unable to traverse outside of this subpath in media gallery
101+
*
102+
* @return string
103+
*/
104+
public function getStorageRootSubpath()
105+
{
106+
return '';
97107
}
98108

99109
/**
@@ -141,7 +151,7 @@ public function convertIdToPath($id)
141151
return $this->getStorageRoot();
142152
} else {
143153
$path = $this->getStorageRoot() . $this->idDecode($id);
144-
if (strpos($path, DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR) !== false) {
154+
if (preg_match('/\.\.(\\\|\/)/', $path)) {
145155
throw new \InvalidArgumentException('Path is invalid');
146156
}
147157

@@ -208,7 +218,7 @@ public function getImageHtmlDeclaration($filename, $renderAsTag = false)
208218
public function getCurrentPath()
209219
{
210220
if (!$this->_currentPath) {
211-
$currentPath = $this->_directory->getAbsolutePath() . \Magento\Cms\Model\Wysiwyg\Config::IMAGE_DIRECTORY;
221+
$currentPath = $this->getStorageRoot();
212222
$path = $this->_getRequest()->getParam($this->getTreeNodeName());
213223
if ($path) {
214224
$path = $this->convertIdToPath($path);
@@ -244,7 +254,7 @@ public function getCurrentUrl()
244254
)->getBaseUrl(
245255
\Magento\Framework\UrlInterface::URL_TYPE_MEDIA
246256
);
247-
$this->_currentUrl = $mediaUrl . $this->_directory->getRelativePath($path) . '/';
257+
$this->_currentUrl = rtrim($mediaUrl . $this->_directory->getRelativePath($path), '/') . '/';
248258
}
249259
return $this->_currentUrl;
250260
}
@@ -261,7 +271,7 @@ public function idEncode($string)
261271
}
262272

263273
/**
264-
* Revert opration to idEncode
274+
* Revert operation to idEncode
265275
*
266276
* @param string $string
267277
* @return string

app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,12 @@ protected function getConditionsForExcludeDirs()
243243
protected function removeItemFromCollection($collection, $conditions)
244244
{
245245
$regExp = $conditions['reg_exp'] ? '~' . implode('|', array_keys($conditions['reg_exp'])) . '~i' : null;
246-
$storageRootLength = strlen($this->_cmsWysiwygImages->getStorageRoot());
246+
$storageRoot = $this->_cmsWysiwygImages->getStorageRoot();
247+
$storageRootLength = strlen($storageRoot);
247248

248249
foreach ($collection as $key => $value) {
249-
$rootChildParts = explode('/', substr($value->getFilename(), $storageRootLength));
250+
$mediaSubPathname = substr($value->getFilename(), $storageRootLength);
251+
$rootChildParts = explode('/', '/' . ltrim($mediaSubPathname, '/'));
250252

251253
if (array_key_exists($rootChildParts[1], $conditions['plain'])
252254
|| ($regExp && preg_match($regExp, $value->getFilename()))) {
@@ -321,6 +323,8 @@ public function getFilesCollection($path, $type = null)
321323
$item->setName($item->getBasename());
322324
$item->setShortName($this->_cmsWysiwygImages->getShortFilename($item->getBasename()));
323325
$item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename());
326+
$item->setSize(filesize($item->getFilename()));
327+
$item->setMimeType(\mime_content_type($item->getFilename()));
324328

325329
if ($this->isImage($item->getBasename())) {
326330
$thumbUrl = $this->getThumbnailUrl($item->getFilename(), true);
@@ -412,7 +416,7 @@ public function createDirectory($name, $path)
412416
/**
413417
* Recursively delete directory from storage
414418
*
415-
* @param string $path Target dir
419+
* @param string $path Absolute path to target directory
416420
* @return void
417421
* @throws \Magento\Framework\Exception\LocalizedException
418422
*/
@@ -421,12 +425,19 @@ public function deleteDirectory($path)
421425
if ($this->_coreFileStorageDb->checkDbUsage()) {
422426
$this->_directoryDatabaseFactory->create()->deleteDirectory($path);
423427
}
428+
if (!$this->isPathAllowed($path, $this->getConditionsForExcludeDirs())) {
429+
throw new \Magento\Framework\Exception\LocalizedException(
430+
__('We cannot delete directory %1.', $this->_getRelativePathToRoot($path))
431+
);
432+
}
424433
try {
425434
$this->_deleteByPath($path);
426435
$path = $this->getThumbnailRoot() . $this->_getRelativePathToRoot($path);
427436
$this->_deleteByPath($path);
428437
} catch (\Magento\Framework\Exception\FileSystemException $e) {
429-
throw new \Magento\Framework\Exception\LocalizedException(__('We cannot delete directory %1.', $path));
438+
throw new \Magento\Framework\Exception\LocalizedException(
439+
__('We cannot delete directory %1.', $this->_getRelativePathToRoot($path))
440+
);
430441
}
431442
}
432443

@@ -473,14 +484,18 @@ public function deleteFile($target)
473484
/**
474485
* Upload and resize new file
475486
*
476-
* @param string $targetPath Target directory
487+
* @param string $targetPath Absolute path to target directory
477488
* @param string $type Type of storage, e.g. image, media etc.
478489
* @return array File info Array
479490
* @throws \Magento\Framework\Exception\LocalizedException
480-
* @throws \Exception
481491
*/
482492
public function uploadFile($targetPath, $type = null)
483493
{
494+
if (!$this->isPathAllowed($targetPath, $this->getConditionsForExcludeDirs())) {
495+
throw new \Magento\Framework\Exception\LocalizedException(
496+
__('We can\'t upload the file to current folder right now. Please try another folder.')
497+
);
498+
}
484499
/** @var \Magento\MediaStorage\Model\File\Uploader $uploader */
485500
$uploader = $this->_uploaderFactory->create(['fileId' => 'image']);
486501
$allowed = $this->getAllowedExtensions($type);
@@ -748,7 +763,7 @@ protected function _getRelativePathToRoot($path)
748763
}
749764

750765
/**
751-
* Prepare mime types config settings
766+
* Prepare mime types config settings.
752767
*
753768
* @param string|null $type Type of storage, e.g. image, media etc.
754769
* @return array Array of allowed file extensions
@@ -761,7 +776,7 @@ private function getAllowedMimeTypes($type = null): array
761776
}
762777

763778
/**
764-
* Get list of allowed file extensions with mime type in values
779+
* Get list of allowed file extensions with mime type in values.
765780
*
766781
* @param string|null $type
767782
* @return array
@@ -775,4 +790,29 @@ private function getExtensionsList($type = null): array
775790
}
776791
return $allowed;
777792
}
793+
794+
/**
795+
* Check if path is not in excluded dirs.
796+
*
797+
* @param string $path Absolute path
798+
* @param array $conditions Exclude conditions
799+
* @return bool
800+
*/
801+
private function isPathAllowed($path, array $conditions): bool
802+
{
803+
$isAllowed = true;
804+
$regExp = $conditions['reg_exp'] ? '~' . implode('|', array_keys($conditions['reg_exp'])) . '~i' : null;
805+
$storageRoot = $this->_cmsWysiwygImages->getStorageRoot();
806+
$storageRootLength = strlen($storageRoot);
807+
808+
$mediaSubPathname = substr($path, $storageRootLength);
809+
$rootChildParts = explode('/', '/' . ltrim($mediaSubPathname, '/'));
810+
811+
if (array_key_exists($rootChildParts[1], $conditions['plain'])
812+
|| ($regExp && preg_match($regExp, $path))) {
813+
$isAllowed = false;
814+
}
815+
816+
return $isAllowed;
817+
}
778818
}

app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class StorageTest extends \PHPUnit\Framework\TestCase
1818
/**
1919
* Directory paths samples
2020
*/
21-
const STORAGE_ROOT_DIR = '/storage/root/dir';
21+
const STORAGE_ROOT_DIR = '/storage/root/dir/';
2222

2323
const INVALID_DIRECTORY_OVER_ROOT = '/storage/some/another/dir';
2424

@@ -434,10 +434,11 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e
434434

435435
public function testUploadFile()
436436
{
437-
$targetPath = '/target/path';
437+
$path = 'target/path';
438+
$targetPath = self::STORAGE_ROOT_DIR . $path;
438439
$fileName = 'image.gif';
439440
$realPath = $targetPath . '/' . $fileName;
440-
$thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs';
441+
$thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs' . $path;
441442
$thumbnailDestination = $thumbnailTargetPath . '/' . $fileName;
442443
$type = 'image';
443444
$result = [

app/code/Magento/Cms/etc/di.xml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,41 @@
5151
</item>
5252
</argument>
5353
<argument name="dirs" xsi:type="array">
54-
<item name="exclude" xsi:type="string"/>
55-
<item name="include" xsi:type="string"/>
54+
<item name="exclude" xsi:type="array">
55+
<item name="captcha" xsi:type="array">
56+
<item name="regexp" xsi:type="boolean">true</item>
57+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+captcha[/\\]*$</item>
58+
</item>
59+
<item name="catalog/product" xsi:type="array">
60+
<item name="regexp" xsi:type="boolean">true</item>
61+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+catalog[/\\]+product[/\\]*$</item>
62+
</item>
63+
<item name="customer" xsi:type="array">
64+
<item name="regexp" xsi:type="boolean">true</item>
65+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+customer[/\\]*$</item>
66+
</item>
67+
<item name="downloadable" xsi:type="array">
68+
<item name="regexp" xsi:type="boolean">true</item>
69+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+downloadable[/\\]*$</item>
70+
</item>
71+
<item name="import" xsi:type="array">
72+
<item name="regexp" xsi:type="boolean">true</item>
73+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+import[/\\]*$</item>
74+
</item>
75+
<item name="theme" xsi:type="array">
76+
<item name="regexp" xsi:type="boolean">true</item>
77+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme[/\\]*$</item>
78+
</item>
79+
<item name="theme_customization" xsi:type="array">
80+
<item name="regexp" xsi:type="boolean">true</item>
81+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme_customization[/\\]*$</item>
82+
</item>
83+
<item name="tmp" xsi:type="array">
84+
<item name="regexp" xsi:type="boolean">true</item>
85+
<item name="name" xsi:type="string">pub[/\\]+media[/\\]+tmp[/\\]*$</item>
86+
</item>
87+
</item>
88+
<item name="include" xsi:type="array"/>
5689
</argument>
5790
</arguments>
5891
</type>

dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images;
88

99
use Magento\Framework\App\Filesystem\DirectoryList;
10+
use Magento\Framework\App\Response\HttpFactory as ResponseFactory;
1011

1112
/**
1213
* Test for \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFolder class.
@@ -28,11 +29,21 @@ class DeleteFolderTest extends \PHPUnit\Framework\TestCase
2829
*/
2930
private $mediaDirectory;
3031

32+
/**
33+
* @var string
34+
*/
35+
private $fullDirectoryPath;
36+
3137
/**
3238
* @var \Magento\Framework\Filesystem
3339
*/
3440
private $filesystem;
3541

42+
/**
43+
* @var ResponseFactory
44+
*/
45+
private $responseFactory;
46+
3647
/**
3748
* @inheritdoc
3849
*/
@@ -41,14 +52,18 @@ protected function setUp()
4152
$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
4253
$this->filesystem = $objectManager->get(\Magento\Framework\Filesystem::class);
4354
$this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
44-
/** @var \Magento\Cms\Helper\Wysiwyg\Images $imagesHelper */
4555
$this->imagesHelper = $objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class);
56+
$this->fullDirectoryPath = $this->imagesHelper->getStorageRoot();
57+
$this->responseFactory = $objectManager->get(ResponseFactory::class);
58+
/** @var \Magento\Cms\Helper\Wysiwyg\Images $imagesHelper */
4659
$this->model = $objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFolder::class);
4760
}
4861

4962
/**
5063
* Execute method with correct directory path to check that directories under WYSIWYG media directory
5164
* can be removed.
65+
*
66+
* @magentoAppIsolation enabled
5267
*/
5368
public function testExecute()
5469
{
@@ -74,6 +89,7 @@ public function testExecute()
7489
* can be removed.
7590
*
7691
* @magentoDataFixture Magento/Cms/_files/linked_media.php
92+
* @magentoAppIsolation enabled
7793
*/
7894
public function testExecuteWithLinkedMedia()
7995
{
@@ -85,11 +101,39 @@ public function testExecuteWithLinkedMedia()
85101
$linkedDirectory->create(
86102
$linkedDirectory->getRelativePath($linkedDirectoryPath . DIRECTORY_SEPARATOR . $directoryName)
87103
);
88-
$this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]);
104+
$this->model->getRequest()->setParams(
105+
['node' => $this->imagesHelper->idEncode('wysiwyg' . DIRECTORY_SEPARATOR . $directoryName)]
106+
);
107+
$this->model->getRequest()->setMethod('POST');
89108
$this->model->execute();
90109
$this->assertFalse(is_dir($linkedDirectoryPath . DIRECTORY_SEPARATOR . $directoryName));
91110
}
92111

112+
/**
113+
* Execute method to check that there is no ability to remove folder which is in excluded directories list.
114+
*
115+
* @return void
116+
* @magentoAppIsolation enabled
117+
*/
118+
public function testExecuteWithExcludedDirectoryName()
119+
{
120+
$directoryName = 'downloadable';
121+
$expectedResponseMessage = 'We cannot delete directory /downloadable.';
122+
$mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
123+
$mediaDirectory->create($directoryName);
124+
$this->assertFileExists($this->fullDirectoryPath . DIRECTORY_SEPARATOR . $directoryName);
125+
126+
$this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]);
127+
$this->model->getRequest()->setMethod('POST');
128+
$jsonResponse = $this->model->execute();
129+
$jsonResponse->renderResult($response = $this->responseFactory->create());
130+
$data = json_decode($response->getBody(), true);
131+
132+
$this->assertTrue($data['error']);
133+
$this->assertEquals($expectedResponseMessage, $data['message']);
134+
$this->assertFileExists($this->fullDirectoryPath . $directoryName);
135+
}
136+
93137
/**
94138
* @inheritdoc
95139
*/
@@ -102,5 +146,8 @@ public static function tearDownAfterClass()
102146
if ($directory->isExist('wysiwyg')) {
103147
$directory->delete('wysiwyg');
104148
}
149+
if ($directory->isExist('downloadable')) {
150+
$directory->delete('downloadable');
151+
}
105152
}
106153
}

0 commit comments

Comments
 (0)