Skip to content

Commit 67681a5

Browse files
committed
Merge remote-tracking branch 'origin/MAGETWO-95236' into 2.2.7-develop-pr53
2 parents 5bc4042 + 116a3b7 commit 67681a5

File tree

8 files changed

+208
-46
lines changed

8 files changed

+208
-46
lines changed

app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
namespace Magento\AdminNotification\Block\Grid\Renderer;
1010

11+
/**
12+
* Renderer class for action in the admin notifications grid
13+
*/
1114
class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer
1215
{
1316
/**
@@ -37,7 +40,8 @@ public function __construct(
3740
*/
3841
public function render(\Magento\Framework\DataObject $row)
3942
{
40-
$readDetailsHtml = $row->getUrl() ? '<a class="action-details" target="_blank" href="' . $row->getUrl() . '">' .
43+
$readDetailsHtml = $row->getUrl() ? '<a class="action-details" target="_blank" href="' .
44+
$this->escapeUrl($row->getUrl()) . '">' .
4145
__('Read Details') . '</a>' : '';
4246

4347
$markAsReadHtml = !$row->getIsRead() ? '<a class="action-mark" href="' . $this->getUrl(

app/code/Magento/Sales/Helper/Admin.php

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
67
namespace Magento\Sales\Helper;
78

9+
/**
10+
* Sales admin helper.
11+
*/
812
class Admin extends \Magento\Framework\App\Helper\AbstractHelper
913
{
1014
/**
@@ -148,22 +152,15 @@ public function escapeHtmlWithLinks($data, $allowedTags = null)
148152
$links = [];
149153
$i = 1;
150154
$data = str_replace('%', '%%', $data);
151-
$regexp = "/<a\s[^>]*href\s*?=\s*?([\"\']??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU";
155+
$regexp = "#(?J)<a"
156+
."(?:(?:\s+(?:(?:href\s*=\s*(['\"])(?<link>.*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)"
157+
.">?(?:(?:(?<text>.*?)(?:<\/a\s*>?|(?=<\w))|(?<text>.*)))#si";
152158
while (preg_match($regexp, $data, $matches)) {
153-
//Revert the sprintf escaping
154-
$url = str_replace('%%', '%', $matches[2]);
155-
$text = str_replace('%%', '%', $matches[3]);
156-
//Check for an valid url
157-
if ($url) {
158-
$urlScheme = strtolower(parse_url($url, PHP_URL_SCHEME));
159-
if ($urlScheme !== 'http' && $urlScheme !== 'https') {
160-
$url = null;
161-
}
162-
}
163-
//Use hash tag as fallback
164-
if (!$url) {
165-
$url = '#';
159+
$text = '';
160+
if (!empty($matches['text'])) {
161+
$text = str_replace('%%', '%', $matches['text']);
166162
}
163+
$url = $this->filterUrl($matches['link'] ?? '');
167164
//Recreate a minimalistic secure a tag
168165
$links[] = sprintf(
169166
'<a href="%s">%s</a>',
@@ -178,4 +175,29 @@ public function escapeHtmlWithLinks($data, $allowedTags = null)
178175
}
179176
return $this->escaper->escapeHtml($data, $allowedTags);
180177
}
178+
179+
/**
180+
* Filter the URL for allowed protocols.
181+
*
182+
* @param string $url
183+
* @return string
184+
*/
185+
private function filterUrl(string $url): string
186+
{
187+
if ($url) {
188+
//Revert the sprintf escaping
189+
$url = str_replace('%%', '%', $url);
190+
$urlScheme = parse_url($url, PHP_URL_SCHEME);
191+
$urlScheme = $urlScheme ? strtolower($urlScheme) : '';
192+
if ($urlScheme !== 'http' && $urlScheme !== 'https') {
193+
$url = null;
194+
}
195+
}
196+
197+
if (!$url) {
198+
$url = '#';
199+
}
200+
201+
return $url;
202+
}
181203
}

app/code/Magento/Widget/Block/Adminhtml/Widget.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
*
1313
* @api
1414
* @since 100.0.2
15+
* @SuppressWarnings(PHPMD.RequestAwareBlockMethod)
1516
*/
1617
class Widget extends \Magento\Backend\Block\Widget\Form\Container
1718
{
1819
/**
19-
* @return void
20+
* @inheritdoc
2021
*/
2122
protected function _construct()
2223
{
@@ -36,12 +37,16 @@ protected function _construct()
3637
$this->buttonList->update('save', 'region', 'footer');
3738
$this->buttonList->update('save', 'data_attribute', []);
3839

39-
$this->_formScripts[] = 'require(["mage/adminhtml/wysiwyg/widget"], function(){wWidget = new WysiwygWidget.Widget(' .
40-
'"widget_options_form", "select_widget_type", "widget_options", "' .
41-
$this->getUrl(
42-
'adminhtml/*/loadOptions'
43-
) . '", "' . $this->getRequest()->getParam(
44-
'widget_target_id'
45-
) . '");});';
40+
$this->_formScripts[] = <<<EOJS
41+
require(['mage/adminhtml/wysiwyg/widget'], function() {
42+
wWidget = new WysiwygWidget.Widget(
43+
'widget_options_form',
44+
'select_widget_type',
45+
'widget_options',
46+
'{$this->getUrl('adminhtml/*/loadOptions')}',
47+
'{$this->escapeJs($this->getRequest()->getParam('widget_target_id'))}'
48+
);
49+
});
50+
EOJS;
4651
}
4752
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\CodeMessDetector\Rule\Design;
10+
11+
use PHPMD\AbstractNode;
12+
use PHPMD\AbstractRule;
13+
use PHPMD\Node\ClassNode;
14+
use PHPMD\Node\MethodNode;
15+
use PDepend\Source\AST\ASTMethod;
16+
use PHPMD\Rule\MethodAware;
17+
18+
/**
19+
* Detect direct request usages.
20+
*/
21+
class RequestAwareBlockMethod extends AbstractRule implements MethodAware
22+
{
23+
/**
24+
* @inheritDoc
25+
*
26+
* @param ASTMethod|MethodNode $method
27+
*/
28+
public function apply(AbstractNode $method)
29+
{
30+
$definedIn = $method->getParentType();
31+
try {
32+
$isBlock = ($definedIn instanceof ClassNode)
33+
&& is_subclass_of(
34+
$definedIn->getFullQualifiedName(),
35+
\Magento\Framework\View\Element\AbstractBlock::class
36+
);
37+
} catch (\Throwable $exception) {
38+
//Failed to load classes.
39+
return;
40+
}
41+
42+
if ($isBlock) {
43+
$nodes = $method->findChildrenOfType('PropertyPostfix') + $method->findChildrenOfType('MethodPostfix');
44+
foreach ($nodes as $node) {
45+
$name = mb_strtolower($node->getFirstChildOfType('Identifier')->getImage());
46+
if ($name === '_request' || $name === 'getrequest') {
47+
$this->addViolation($method, [$method->getFullQualifiedName()]);
48+
break;
49+
}
50+
}
51+
}
52+
}
53+
}

dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@ final class Foo
2929
}
3030
class Baz {
3131
final public function bad() {}
32+
}
33+
]]>
34+
</example>
35+
</rule>
36+
<rule name="RequestAwareBlockMethod"
37+
class="Magento\CodeMessDetector\Rule\Design\RequestAwareBlockMethod"
38+
message="{0} uses request object directly. Add user input validation and suppress this warning.">
39+
<description>
40+
<![CDATA[
41+
Blocks must not depend on being used with certain controllers.
42+
If you use request object in a block directly you must validate all user input inside the block.
43+
]]>
44+
</description>
45+
<priority>2</priority>
46+
<properties />
47+
<example>
48+
<![CDATA[
49+
class MyOrder extends AbstractBlock
50+
{
51+
52+
.......
53+
54+
public function getOrder()
55+
{
56+
$orderId = $this->getRequest()->getParam('order_id');
57+
//Validate customer having such order.
58+
if (!$this->hasOrder($this->getCustomerId(), $orderId)) {
59+
...deny access...
60+
}
61+
.....
62+
}
3263
}
3364
]]>
3465
</example>

dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,5 @@
4747

4848
<!-- Magento Specific Rules -->
4949
<rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/FinalImplementation" />
50-
50+
<rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/RequestAwareBlockMethod" />
5151
</ruleset>

lib/internal/Magento/Framework/Escaper.php

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
67
namespace Magento\Framework;
78

89
/**
@@ -32,38 +33,43 @@ class Escaper
3233
*/
3334
private $allowedAttributes = ['id', 'class', 'href', 'target', 'title', 'style'];
3435

36+
/**
37+
* @var string
38+
*/
39+
private static $xssFiltrationPattern =
40+
'/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|'
41+
. '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|'
42+
. '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i';
43+
3544
/**
3645
* @var string[]
3746
*/
3847
private $escapeAsUrlAttributes = ['href'];
3948

4049
/**
41-
* Escape string for HTML context. allowedTags will not be escaped, except the following: script, img, embed,
42-
* iframe, video, source, object, audio
50+
* Escape string for HTML context.
51+
*
52+
* AllowedTags will not be escaped, except the following: script, img, embed,
53+
* iframe, video, source, object, audio.
4354
*
4455
* @param string|array $data
4556
* @param array|null $allowedTags
4657
* @return string|array
4758
*/
4859
public function escapeHtml($data, $allowedTags = null)
4960
{
61+
if (!is_array($data)) {
62+
$data = (string)$data;
63+
}
64+
5065
if (is_array($data)) {
5166
$result = [];
5267
foreach ($data as $item) {
5368
$result[] = $this->escapeHtml($item, $allowedTags);
5469
}
5570
} elseif (strlen($data)) {
5671
if (is_array($allowedTags) && !empty($allowedTags)) {
57-
$notAllowedTags = array_intersect(
58-
array_map('strtolower', $allowedTags),
59-
$this->notAllowedTags
60-
);
61-
if (!empty($notAllowedTags)) {
62-
$this->getLogger()->critical(
63-
'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags)
64-
);
65-
$allowedTags = array_diff($allowedTags, $this->notAllowedTags);
66-
}
72+
$allowedTags = $this->filterProhibitedTags($allowedTags);
6773
$wrapperElementId = uniqid();
6874
$domDocument = new \DOMDocument('1.0', 'UTF-8');
6975
set_error_handler(
@@ -197,7 +203,7 @@ public function escapeHtmlAttr($string, $escapeSingleQuote = true)
197203
if ($escapeSingleQuote) {
198204
return $this->getEscaper()->escapeHtmlAttr((string) $string);
199205
}
200-
return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
206+
return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8', false);
201207
}
202208

203209
/**
@@ -278,30 +284,47 @@ public function escapeJsQuote($data, $quote = '\'')
278284
$result[] = $this->escapeJsQuote($item, $quote);
279285
}
280286
} else {
281-
$result = str_replace($quote, '\\' . $quote, $data);
287+
$result = str_replace($quote, '\\' . $quote, (string)$data);
282288
}
283289
return $result;
284290
}
285291

286292
/**
287293
* Escape xss in urls
288-
* Remove `javascript:`, `vbscript:`, `data:` words from url
289294
*
290295
* @param string $data
291296
* @return string
292297
* @deprecated 100.2.0
293298
*/
294299
public function escapeXssInUrl($data)
295300
{
296-
$pattern = '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|'
297-
. '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|'
298-
. '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i';
299-
$result = preg_replace($pattern, ':', $data);
300-
return htmlspecialchars($result, ENT_COMPAT | ENT_HTML5 | ENT_HTML401, 'UTF-8', false);
301+
return htmlspecialchars(
302+
$this->escapeScriptIdentifiers((string)$data),
303+
ENT_COMPAT | ENT_HTML5 | ENT_HTML401,
304+
'UTF-8',
305+
false
306+
);
307+
}
308+
309+
/**
310+
* Remove `javascript:`, `vbscript:`, `data:` words from the string.
311+
*
312+
* @param string $data
313+
* @return string
314+
*/
315+
private function escapeScriptIdentifiers(string $data): string
316+
{
317+
$filteredData = preg_replace(self::$xssFiltrationPattern, ':', $data) ?: '';
318+
if (preg_match(self::$xssFiltrationPattern, $filteredData)) {
319+
$filteredData = $this->escapeScriptIdentifiers($filteredData);
320+
}
321+
322+
return $filteredData;
301323
}
302324

303325
/**
304326
* Escape quotes inside html attributes
327+
*
305328
* Use $addSlashes = false for escaping js that inside html attribute (onClick, onSubmit etc)
306329
*
307330
* @param string $data
@@ -346,4 +369,27 @@ private function getLogger()
346369
}
347370
return $this->logger;
348371
}
372+
373+
/**
374+
* Filter prohibited tags.
375+
*
376+
* @param string[] $allowedTags
377+
* @return string[]
378+
*/
379+
private function filterProhibitedTags(array $allowedTags): array
380+
{
381+
$notAllowedTags = array_intersect(
382+
array_map('strtolower', $allowedTags),
383+
$this->notAllowedTags
384+
);
385+
386+
if (!empty($notAllowedTags)) {
387+
$this->getLogger()->critical(
388+
'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags)
389+
);
390+
$allowedTags = array_diff($allowedTags, $this->notAllowedTags);
391+
}
392+
393+
return $allowedTags;
394+
}
349395
}

0 commit comments

Comments
 (0)