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