Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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(): void
  65      {
  66          if (Settings::getLibXmlDisableEntityLoader() && \PHP_VERSION_ID < 80000) {
  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(): void
  76      {
  77          if (self::$libxmlDisableEntityLoaderValue !== null && \PHP_VERSION_ID < 80000) {
  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): void
  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       * @return string
 118       */
 119      public function scan($xml)
 120      {
 121          $this->disableEntityLoaderCheck();
 122  
 123          $xml = $this->toUtf8($xml);
 124  
 125          // Don't rely purely on libxml_disable_entity_loader()
 126          $pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
 127  
 128          if (preg_match($pattern, $xml)) {
 129              throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
 130          }
 131  
 132          if ($this->callback !== null && is_callable($this->callback)) {
 133              $xml = call_user_func($this->callback, $xml);
 134          }
 135  
 136          return $xml;
 137      }
 138  
 139      /**
 140       * Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
 141       *
 142       * @param string $filestream
 143       *
 144       * @return string
 145       */
 146      public function scanFile($filestream)
 147      {
 148          return $this->scan(file_get_contents($filestream));
 149      }
 150  }