12 class XssOutputValidator
14 const ESCAPE_NOT_VERIFIED_PATTERN =
'/\* @escapeNotVerified \*/';
16 const ESCAPED_PATTERN =
'/\* @noEscape \*/';
23 private $origins = [];
30 private $replacements = [];
37 private $escapeFunctions = [
'escapeHtml',
'escapeHtmlAttr',
'escapeUrl',
'escapeJs',
'escapeCss'];
44 public function getLinesWithXssSensitiveOutput($file)
47 $xssUnsafeBlocks = $this->getXssUnsafeBlocks($fileContent);
50 foreach ($xssUnsafeBlocks as
$block) {
51 $lines = array_merge($lines, $this->findBlockLineNumbers(
$block, $fileContent));
55 $lines = array_unique($lines);
57 return implode(
',', $lines);
74 while (
$pos !==
false) {
76 if ($this->isNotEscapeMarkedBlock($contentBeforeString)
77 && $this->isNotInCommentBlock($contentBeforeString)
79 $results[] = count(explode(PHP_EOL, $contentBeforeString));
93 public function getXssUnsafeBlocks($fileContent)
97 $fileContent = $this->replacePhpQuoteWithPlaceholders($fileContent);
98 $fileContent = $this->replacePhpCommentsWithPlaceholders($fileContent);
100 $this->addOriginReplacement(
'\'\
'',
"'-*=single=*-'");
101 $this->addOriginReplacement(
'""',
'"-*=double=*-"');
103 if (preg_match_all(
'/<[?](php|=)(.*?)[?]>/sm', $fileContent, $phpBlockMatches)) {
104 foreach ($phpBlockMatches[2] as
$index => $phpBlock) {
105 $phpCommands = explode(
';', $phpBlock);
106 if ($phpBlockMatches[1][
$index] ==
'php') {
107 $echoCommands = preg_grep(
'#( |^|/\*.*?\*/)echo[\s(]+.*#sm', $phpCommands);
109 $echoCommands[] = $phpBlockMatches[0][
$index];
113 $this->getEchoUnsafeCommands($echoCommands)
118 $this->clearOriginReplacements();
128 private function getEchoUnsafeCommands(array $echoCommands)
131 foreach ($echoCommands as $echoCommand) {
132 if ($this->isNotEscapeMarkedCommand($echoCommand)) {
133 $echoCommand = preg_replace(
'/^(.*?)echo/sim',
'echo', $echoCommand);
134 $preparedEchoCommand = $this->prepareEchoCommand($echoCommand);
135 $isEscapeFunctionArgument = preg_match(
136 '/->(' . implode(
'|', $this->escapeFunctions) .
')\(.*?\)$/sim',
139 $xssUnsafeCommands = array_filter(
140 $isEscapeFunctionArgument ? [$preparedEchoCommand] : explode(
'.', $preparedEchoCommand),
141 [$this,
'isXssUnsafeCommand']
143 if (count($xssUnsafeCommands)) {
145 $this->getReplacements(),
160 private function prepareEchoCommand($command)
162 $command = preg_replace(
'/<[?]=(.*?)[?]>/sim',
'\1', $command);
163 return trim(ltrim(explode(
';', $command)[0],
'echo'));
170 private function isNotEscapeMarkedBlock($contentBeforeString)
173 '%(' . self::ESCAPE_NOT_VERIFIED_PATTERN .
'|' . self::ESCAPED_PATTERN .
')$%sim',
174 trim($contentBeforeString)
182 private function isNotInCommentBlock($contentBeforeString)
184 $contentBeforeString = explode(
'<?php', $contentBeforeString);
185 $contentBeforeString = preg_replace(
188 end($contentBeforeString)
191 return (strpos($contentBeforeString,
'/*') ===
false);
198 private function isNotEscapeMarkedCommand($command)
201 '%' . self::ESCAPE_NOT_VERIFIED_PATTERN .
'|' . self::ESCAPED_PATTERN .
'%sim',
212 public function isXssUnsafeCommand($command)
214 $command = trim($command);
218 '/->(' . implode(
'|', $this->escapeFunctions) .
'|.*html.*)\(/simU',
219 $this->getLastMethod($command)
222 case preg_match(
'/^\((int|bool|float)\)/sim', $command):
224 case preg_match(
'/^count\(/sim', $command):
226 case preg_match(
"/^'.*'$/sim", $command):
228 case preg_match(
'/^".*?"$/sim', $command, $matches):
229 return $this->isContainPhpVariables($this->getOrigin($matches[0]));
239 private function getLastMethod($command)
243 $this->clearMethodBracketContent($command),
246 $command = end($matches[0]);
247 $command = substr($command, 0, strpos($command,
'(') + 1);
257 private function clearMethodBracketContent($command)
259 $bracketInterval = [];
260 $bracketOpenPos = [];
261 $command = str_split($command);
262 foreach ($command as
$index => $character) {
263 if ($character ==
'(') {
264 array_push($bracketOpenPos,
$index);
266 if (count($bracketOpenPos)) {
267 if ($character ==
')') {
268 $lastOpenPos = array_pop($bracketOpenPos);
269 if (count($bracketOpenPos) == 0) {
270 $bracketInterval[] = [$lastOpenPos,
$index];
275 foreach ($bracketInterval as $interval) {
276 for (
$i = $interval[0] + 1;
$i < $interval[1];
$i++) {
280 $command = implode(
'', $command);
289 private function isContainPhpVariables(
$content)
291 return preg_match(
'/[^\\\\]\$[a-z_\x7f-\xff]/sim',
$content);
298 private function replacePhpQuoteWithPlaceholders($fileContent)
302 if (preg_match_all(
'/<[?](php|=)(.*?)[?]>/sm', $fileContent, $phpBlockMatches)) {
303 foreach ($phpBlockMatches[2] as $phpBlock) {
304 $phpBlockQuoteReplaced = preg_replace(
305 [
'/([^\\\\])\'\'/si',
'/([^\\\\])""/si'],
306 [
"\1'-*=single=*-'",
'\1"-*=double=*-"'],
310 $this->addQuoteOriginsReplacements(
311 $phpBlockQuoteReplaced,
313 '/([^\\\\])([\'])(.*?)([^\\\\])([\'])/sim' 316 $this->addQuoteOriginsReplacements(
317 $phpBlockQuoteReplaced,
319 '/([^\\\\])(["])(.*?)([^\\\\])(["])/sim',
323 $origins[] = $phpBlock;
324 $replacements[] = str_replace(
326 $this->getReplacements(),
327 $phpBlockQuoteReplaced
332 return str_replace($origins, $replacements, $fileContent);
339 private function replacePhpCommentsWithPlaceholders($fileContent)
343 if (preg_match_all(
'%/\*.*?\*/%simu', $fileContent, $docCommentMatches, PREG_SET_ORDER)) {
344 foreach ($docCommentMatches as $docCommentMatch) {
345 if ($this->isNotEscapeMarkedCommand($docCommentMatch[0])
346 && !$this->issetOrigin($docCommentMatch[0])) {
347 $origin = $docCommentMatch[0];
348 $replacement =
'-*!' . count($this->getOrigins()) .
'!*-';
349 $origins[] = $origin;
351 $this->addOriginReplacement(
359 return str_replace($origins, $replacements, $fileContent);
369 private function addQuoteOriginsReplacements($phpBlock, array $patterns)
372 if (preg_match_all(
$pattern, $phpBlock, $quoteMatches, PREG_SET_ORDER)) {
373 foreach ($quoteMatches as $quoteMatch) {
374 $origin = $quoteMatch[2] . $quoteMatch[3] . $quoteMatch[4] . $quoteMatch[5];
375 if (!$this->issetOrigin($origin)) {
376 $this->addOriginReplacement(
378 $quoteMatch[2] .
'-*=' . count($this->getOrigins()) .
'=*-' . $quoteMatch[5]
391 private function addOriginReplacement($origin,
$replacement)
402 private function clearOriginReplacements()
405 $this->replacements = [];
411 private function getOrigins()
413 return $this->origins;
420 private function getOrigin($key)
422 return array_key_exists($key, $this->origins) ? $this->origins[$key] :
null;
429 private function issetOrigin($origin)
431 return in_array($origin, $this->origins);
437 private function getReplacements()
439 return $this->replacements;
return['app/code */registration.php', 'app/design */registration.php', 'lib/internal **/registration.php']