Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Reader\Security;
   4  
   5  use PhpOffice\PhpSpreadsheet\Reader;
   6  use PhpOffice\PhpSpreadsheet\Settings;
   7  
   8  class XmlScanner
   9  {
  10      /**
  11       * String used to identify risky xml elements.
  12       *
  13       * @var string
  14       */
  15      private $pattern;
  16  
  17      private $callback;
  18  
  19      private static $libxmlDisableEntityLoaderValue;
  20  
  21      public function __construct($pattern = '<!DOCTYPE')
  22      {
  23          $this->pattern = $pattern;
  24  
  25          $this->disableEntityLoaderCheck();
  26  
  27          // A fatal error will bypass the destructor, so we register a shutdown here
  28          register_shutdown_function([__CLASS__, 'shutdown']);
  29      }
  30  
  31      public static function getInstance(Reader\IReader $reader)
  32      {
  33          switch (true) {
  34              case $reader instanceof Reader\Html:
  35                  return new self('<!ENTITY');
  36              case $reader instanceof Reader\Xlsx:
  37              case $reader instanceof Reader\Xml:
  38              case $reader instanceof Reader\Ods:
  39              case $reader instanceof Reader\Gnumeric:
  40                  return new self('<!DOCTYPE');
  41              default:
  42                  return new self('<!DOCTYPE');
  43          }
  44      }
  45  
  46      public static function threadSafeLibxmlDisableEntityLoaderAvailability()
  47      {
  48          if (PHP_MAJOR_VERSION == 7) {
  49              switch (PHP_MINOR_VERSION) {
  50                  case 2:
  51                      return PHP_RELEASE_VERSION >= 1;
  52                  case 1:
  53                      return PHP_RELEASE_VERSION >= 13;
  54                  case 0:
  55                      return PHP_RELEASE_VERSION >= 27;
  56              }
  57  
  58              return true;
  59          }
  60  
  61          return false;
  62      }
  63  
  64      private function disableEntityLoaderCheck()
  65      {
  66          if (Settings::getLibXmlDisableEntityLoader()) {
  67              $libxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
  68  
  69              if (self::$libxmlDisableEntityLoaderValue === null) {
  70                  self::$libxmlDisableEntityLoaderValue = $libxmlDisableEntityLoaderValue;
  71              }
  72          }
  73      }
  74  
  75      public static function shutdown()
  76      {
  77          if (self::$libxmlDisableEntityLoaderValue !== null) {
  78              libxml_disable_entity_loader(self::$libxmlDisableEntityLoaderValue);
  79              self::$libxmlDisableEntityLoaderValue = null;
  80          }
  81      }
  82  
  83      public function __destruct()
  84      {
  85          self::shutdown();
  86      }
  87  
  88      public function setAdditionalCallback(callable $callback)
  89      {
  90          $this->callback = $callback;
  91      }
  92  
  93      private function toUtf8($xml)
  94      {
  95          $pattern = '/encoding="(.*?)"/';
  96          $result = preg_match($pattern, $xml, $matches);
  97          $charset = strtoupper($result ? $matches[1] : 'UTF-8');
  98  
  99          if ($charset !== 'UTF-8') {
 100              $xml = mb_convert_encoding($xml, 'UTF-8', $charset);
 101  
 102              $result = preg_match($pattern, $xml, $matches);
 103              $charset = strtoupper($result ? $matches[1] : 'UTF-8');
 104              if ($charset !== 'UTF-8') {
 105                  throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
 106              }
 107          }
 108  
 109          return $xml;
 110      }
 111  
 112      /**
 113       * Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
 114       *
 115       * @param mixed $xml
 116       *
 117       * @throws Reader\Exception
 118       *
 119       * @return string
 120       */
 121      public function scan($xml)
 122      {
 123          $this->disableEntityLoaderCheck();
 124  
 125          $xml = $this->toUtf8($xml);
 126  
 127          // Don't rely purely on libxml_disable_entity_loader()
 128          $pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
 129  
 130          if (preg_match($pattern, $xml)) {
 131              throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
 132          }
 133  
 134          if ($this->callback !== null && is_callable($this->callback)) {
 135              $xml = call_user_func($this->callback, $xml);
 136          }
 137  
 138          return $xml;
 139      }
 140  
 141      /**
 142       * Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
 143       *
 144       * @param string $filestream
 145       *
 146       * @throws Reader\Exception
 147       *
 148       * @return string
 149       */
 150      public function scanFile($filestream)
 151      {
 152          return $this->scan(file_get_contents($filestream));
 153      }
 154  }