Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Security.php
Go to the documentation of this file.
1 <?php
30 {
31  const ENTITY_DETECT = 'Detected use of ENTITY in XML, disabled to prevent XXE/XEE attacks';
32 
39  protected static function heuristicScan($xml)
40  {
41  foreach (self::getEntityComparison($xml) as $compare) {
42  if (strpos($xml, $compare) !== false) {
43  throw new Zend_Xml_Exception(self::ENTITY_DETECT);
44  }
45  }
46  }
47 
55  public static function loadXmlErrorHandler($errno, $errstr, $errfile, $errline)
56  {
57  if (substr_count($errstr, 'DOMDocument::loadXML()') > 0) {
58  return true;
59  }
60  return false;
61  }
62 
71  public static function scan($xml, DOMDocument $dom = null)
72  {
73  // If running with PHP-FPM we perform an heuristic scan
74  // We cannot use libxml_disable_entity_loader because of this bug
75  // @see https://bugs.php.net/bug.php?id=64938
76  if (self::isPhpFpm()) {
77  self::heuristicScan($xml);
78  }
79 
80  if (null === $dom) {
81  $simpleXml = true;
82  $dom = new DOMDocument();
83  }
84 
85  if (!self::isPhpFpm()) {
86  $loadEntities = libxml_disable_entity_loader(true);
87  $useInternalXmlErrors = libxml_use_internal_errors(true);
88  }
89 
90  // Load XML with network access disabled (LIBXML_NONET)
91  // error disabled with @ for PHP-FPM scenario
92  set_error_handler(array('Zend_Xml_Security', 'loadXmlErrorHandler'), E_WARNING);
93 
94  $result = $dom->loadXml($xml, LIBXML_NONET);
95  restore_error_handler();
96 
97  if (!$result) {
98  // Entity load to previous setting
99  if (!self::isPhpFpm()) {
100  libxml_disable_entity_loader($loadEntities);
101  libxml_use_internal_errors($useInternalXmlErrors);
102  }
103  return false;
104  }
105 
106  // Scan for potential XEE attacks using ENTITY, if not PHP-FPM
107  if (!self::isPhpFpm()) {
108  foreach ($dom->childNodes as $child) {
109  if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
110  if ($child->entities->length > 0) {
111  #require_once 'Exception.php';
112  throw new Zend_Xml_Exception(self::ENTITY_DETECT);
113  }
114  }
115  }
116  }
117 
118  // Entity load to previous setting
119  if (!self::isPhpFpm()) {
120  libxml_disable_entity_loader($loadEntities);
121  libxml_use_internal_errors($useInternalXmlErrors);
122  }
123 
124  if (isset($simpleXml)) {
125  $result = simplexml_import_dom($dom);
126  if (!$result instanceof SimpleXMLElement) {
127  return false;
128  }
129  return $result;
130  }
131  return $dom;
132  }
133 
142  public static function scanFile($file, DOMDocument $dom = null)
143  {
144  if (!file_exists($file)) {
145  #require_once 'Exception.php';
146  throw new Zend_Xml_Exception(
147  "The file $file specified doesn't exist"
148  );
149  }
150  return self::scan(file_get_contents($file), $dom);
151  }
152 
167  public static function isPhpFpm()
168  {
169  $isVulnerableVersion = (
170  version_compare(PHP_VERSION, '5.5.22', 'lt')
171  || (
172  version_compare(PHP_VERSION, '5.6', 'gte')
173  && version_compare(PHP_VERSION, '5.6.6', 'lt')
174  )
175  );
176 
177  if (substr(php_sapi_name(), 0, 3) === 'fpm' && $isVulnerableVersion) {
178  return true;
179  }
180  return false;
181  }
182 
189  protected static function getEntityComparison($xml)
190  {
191  $encodingMap = self::getAsciiEncodingMap();
192  return array_map(
193  array(__CLASS__, 'generateEntityComparison'),
194  self::detectXmlEncoding($xml, self::detectStringEncoding($xml))
195  );
196  }
197 
207  protected static function detectStringEncoding($xml)
208  {
209  $encoding = self::detectBom($xml);
210  return ($encoding) ? $encoding : self::detectXmlStringEncoding($xml);
211  }
212 
223  protected static function detectBom($string)
224  {
225  foreach (self::getBomMap() as $criteria) {
226  if (0 === strncmp($string, $criteria['bom'], $criteria['length'])) {
227  return $criteria['encoding'];
228  }
229  }
230  return false;
231  }
232 
239  protected static function detectXmlStringEncoding($xml)
240  {
241  foreach (self::getAsciiEncodingMap() as $encoding => $generator) {
242  $prefix = call_user_func($generator, '<' . '?xml');
243  if (0 === strncmp($xml, $prefix, strlen($prefix))) {
244  return $encoding;
245  }
246  }
247 
248  // Fallback
249  return 'UTF-8';
250  }
251 
266  protected static function detectXmlEncoding($xml, $fileEncoding)
267  {
268  $encodingMap = self::getAsciiEncodingMap();
269  $generator = $encodingMap[$fileEncoding];
270  $encAttr = call_user_func($generator, 'encoding="');
272  $close = call_user_func($generator, '>');
273 
274  $closePos = strpos($xml, $close);
275  if (false === $closePos) {
276  return array($fileEncoding);
277  }
278 
279  $encPos = strpos($xml, $encAttr);
280  if (false === $encPos
281  || $encPos > $closePos
282  ) {
283  return array($fileEncoding);
284  }
285 
286  $encPos += strlen($encAttr);
287  $quotePos = strpos($xml, $quote, $encPos);
288  if (false === $quotePos) {
289  return array($fileEncoding);
290  }
291 
292  $encoding = self::substr($xml, $encPos, $quotePos);
293  return array(
294  // Following line works because we're only supporting 8-bit safe encodings at this time.
295  str_replace('\0', '', $encoding), // detected encoding
296  $fileEncoding, // file encoding
297  );
298  }
299 
309  protected static function getBomMap()
310  {
311  return array(
312  array(
313  'encoding' => 'UTF-32BE',
314  'bom' => pack('CCCC', 0x00, 0x00, 0xfe, 0xff),
315  'length' => 4,
316  ),
317  array(
318  'encoding' => 'UTF-32LE',
319  'bom' => pack('CCCC', 0xff, 0xfe, 0x00, 0x00),
320  'length' => 4,
321  ),
322  array(
323  'encoding' => 'GB-18030',
324  'bom' => pack('CCCC', 0x84, 0x31, 0x95, 0x33),
325  'length' => 4,
326  ),
327  array(
328  'encoding' => 'UTF-16BE',
329  'bom' => pack('CC', 0xfe, 0xff),
330  'length' => 2,
331  ),
332  array(
333  'encoding' => 'UTF-16LE',
334  'bom' => pack('CC', 0xff, 0xfe),
335  'length' => 2,
336  ),
337  array(
338  'encoding' => 'UTF-8',
339  'bom' => pack('CCC', 0xef, 0xbb, 0xbf),
340  'length' => 3,
341  ),
342  );
343  }
344 
354  protected static function getAsciiEncodingMap()
355  {
356  return array(
357  'UTF-32BE' => array(__CLASS__, 'encodeToUTF32BE'),
358  'UTF-32LE' => array(__CLASS__, 'encodeToUTF32LE'),
359  'UTF-32odd1' => array(__CLASS__, 'encodeToUTF32odd1'),
360  'UTF-32odd2' => array(__CLASS__, 'encodeToUTF32odd2'),
361  'UTF-16BE' => array(__CLASS__, 'encodeToUTF16BE'),
362  'UTF-16LE' => array(__CLASS__, 'encodeToUTF16LE'),
363  'UTF-8' => array(__CLASS__, 'encodeToUTF8'),
364  'GB-18030' => array(__CLASS__, 'encodeToUTF8'),
365  );
366  }
367 
379  protected static function substr($string, $start, $end)
380  {
381  $substr = '';
382  for ($i = $start; $i < $end; $i += 1) {
383  $substr .= $string[$i];
384  }
385  return $substr;
386  }
387 
398  public static function generateEntityComparison($encoding)
399  {
400  $encodingMap = self::getAsciiEncodingMap();
401  $generator = isset($encodingMap[$encoding]) ? $encodingMap[$encoding] : $encodingMap['UTF-8'];
402  return call_user_func($generator, '<!ENTITY');
403  }
404 
412  public static function encodeToUTF32BE($ascii)
413  {
414  return preg_replace('/(.)/', "\0\0\0\\1", $ascii);
415  }
416 
424  public static function encodeToUTF32LE($ascii)
425  {
426  return preg_replace('/(.)/', "\\1\0\0\0", $ascii);
427  }
428 
436  public static function encodeToUTF32odd1($ascii)
437  {
438  return preg_replace('/(.)/', "\0\\1\0\0", $ascii);
439  }
440 
448  public static function encodeToUTF32odd2($ascii)
449  {
450  return preg_replace('/(.)/', "\0\0\\1\0", $ascii);
451  }
452 
460  public static function encodeToUTF16BE($ascii)
461  {
462  return preg_replace('/(.)/', "\0\\1", $ascii);
463  }
464 
472  public static function encodeToUTF16LE($ascii)
473  {
474  return preg_replace('/(.)/', "\\1\0", $ascii);
475  }
476 
484  public static function encodeToUTF8($ascii)
485  {
486  return $ascii;
487  }
488 }
static detectXmlEncoding($xml, $fileEncoding)
Definition: Security.php:266
static isPhpFpm()
Definition: Security.php:167
static getEntityComparison($xml)
Definition: Security.php:189
static encodeToUTF32LE($ascii)
Definition: Security.php:424
static scanFile($file, DOMDocument $dom=null)
Definition: Security.php:142
static scan($xml, DOMDocument $dom=null)
Definition: Security.php:71
static substr($string, $start, $end)
Definition: Security.php:379
static encodeToUTF32BE($ascii)
Definition: Security.php:412
$quote
$start
Definition: listing.phtml:18
static generateEntityComparison($encoding)
Definition: Security.php:398
static getAsciiEncodingMap()
Definition: Security.php:354
const ENTITY_DETECT
Definition: Security.php:31
static encodeToUTF16BE($ascii)
Definition: Security.php:460
$prefix
Definition: name.phtml:25
static detectXmlStringEncoding($xml)
Definition: Security.php:239
static encodeToUTF32odd1($ascii)
Definition: Security.php:436
static encodeToUTF8($ascii)
Definition: Security.php:484
static heuristicScan($xml)
Definition: Security.php:39
static encodeToUTF16LE($ascii)
Definition: Security.php:472
static encodeToUTF32odd2($ascii)
Definition: Security.php:448
static getBomMap()
Definition: Security.php:309
$i
Definition: gallery.phtml:31
static detectBom($string)
Definition: Security.php:223
static detectStringEncoding($xml)
Definition: Security.php:207
static loadXmlErrorHandler($errno, $errstr, $errfile, $errline)
Definition: Security.php:55