PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Reader / Security / XmlScanner.php

XmlScanner.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Reader/Security/XmlScanner.php

198 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace TablePress\PhpOffice\PhpSpreadsheet\Reader\Security;
4
5 use TablePress\PhpOffice\PhpSpreadsheet\Reader;
6
7 class XmlScanner
8 {
9 private const ENCODING_PATTERN = '/encoding\s*=\s*(["\'])(.+?)\1/s';
10 private const ENCODING_UTF7 = '/encoding\s*=\s*(["\'])UTF-7\1/si';
11
12 /**
13 * String used to identify risky xml elements.
14 *
15 * @var string
16 */
17 private $pattern;
18
19 /** @var ?callable */
20 private $callback;
21
22 /** @var ?bool */
23 private static $libxmlDisableEntityLoaderValue;
24
25 /**
26 * @var bool
27 */
28 private static $shutdownRegistered = false;
29
30 public function __construct(string $pattern = '<!DOCTYPE')
31 {
32 $this->pattern = $pattern;
33
34 $this->disableEntityLoaderCheck();
35
36 // A fatal error will bypass the destructor, so we register a shutdown here
37 if (!self::$shutdownRegistered) {
38 self::$shutdownRegistered = true;
39 register_shutdown_function([__CLASS__, 'shutdown']);
40 }
41 }
42
43 public static function getInstance(Reader\IReader $reader): self
44 {
45 $pattern = ($reader instanceof Reader\Html) ? '<!ENTITY' : '<!DOCTYPE';
46
47 return new self($pattern);
48 }
49
50 /**
51 * @codeCoverageIgnore
52 */
53 public static function threadSafeLibxmlDisableEntityLoaderAvailability(): bool
54 {
55 if (PHP_MAJOR_VERSION === 7) {
56 return true;
57 }
58
59 return false;
60 }
61
62 /**
63 * @codeCoverageIgnore
64 */
65 private function disableEntityLoaderCheck(): void
66 {
67 if (\PHP_VERSION_ID < 80000) {
68 $libxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
69
70 if (self::$libxmlDisableEntityLoaderValue === null) {
71 self::$libxmlDisableEntityLoaderValue = $libxmlDisableEntityLoaderValue;
72 }
73 }
74 }
75
76 /**
77 * @codeCoverageIgnore
78 */
79 public static function shutdown(): void
80 {
81 if (self::$libxmlDisableEntityLoaderValue !== null && \PHP_VERSION_ID < 80000) {
82 libxml_disable_entity_loader(self::$libxmlDisableEntityLoaderValue);
83 self::$libxmlDisableEntityLoaderValue = null;
84 }
85 }
86
87 public function __destruct()
88 {
89 self::shutdown();
90 }
91
92 public function setAdditionalCallback(callable $callback): void
93 {
94 $this->callback = $callback;
95 }
96
97 /** @param mixed $arg */
98 private static function forceString($arg): string
99 {
100 return is_string($arg) ? $arg : '';
101 }
102
103 /**
104 * @param string $xml
105 *
106 * @return string
107 */
108 private function toUtf8($xml)
109 {
110 $charset = $this->findCharSet($xml);
111 $foundUtf7 = $charset === 'UTF-7';
112 if ($charset !== 'UTF-8') {
113 $testStart = '/^.{0,4}\s*<?xml/s';
114 $startWithXml1 = preg_match($testStart, $xml);
115 $xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset));
116 if ($startWithXml1 === 1 && preg_match($testStart, $xml) !== 1) {
117 throw new Reader\Exception('Double encoding not permitted');
118 }
119 $foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1);
120 $xml = preg_replace(self::ENCODING_PATTERN, '', $xml) ?? $xml;
121 } else {
122 $foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1);
123 }
124 if ($foundUtf7) {
125 throw new Reader\Exception('UTF-7 encoding not permitted');
126 }
127 if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) {
128 if (preg_match(self::ENCODING_PATTERN, $xml, $matches) === 1) {
129 if (strtolower($matches[2]) !== 'utf-8') {
130 throw new Reader\Exception("BOM says UTF-8 but encoding says {$matches[2]}");
131 }
132 }
133 $xml = (string) substr($xml, Reader\Csv::UTF8_BOM_LEN);
134 }
135
136 return $xml;
137 }
138
139 private function findCharSet(string $xml): string
140 {
141 if (str_starts_with($xml, "\x4c\x6f\xa7\x94")) {
142 throw new Reader\Exception('EBCDIC encoding not permitted');
143 }
144 $encoding = Reader\Csv::guessEncodingBom('', $xml);
145 if ($encoding !== '') {
146 return $encoding;
147 }
148 $xml = str_replace("\0", '', $xml);
149 if (preg_match(self::ENCODING_PATTERN, $xml, $matches)) {
150 return strtoupper($matches[2]);
151 }
152
153 return 'UTF-8';
154 }
155
156 /**
157 * Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
158 *
159 * @param false|string $xml
160 *
161 * @return string
162 */
163 public function scan($xml)
164 {
165 // Don't rely purely on libxml_disable_entity_loader()
166 $pattern = '/\0*' . implode('\0*', mb_str_split($this->pattern, 1, 'UTF-8')) . '\0*/';
167
168 $xml = "$xml";
169 if (preg_match($pattern, $xml)) {
170 throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
171 }
172
173 $xml = $this->toUtf8($xml);
174 if (preg_match($pattern, $xml)) {
175 throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
176 }
177
178 if ($this->callback !== null) {
179 $xml = call_user_func($this->callback, $xml);
180 }
181 /** @var string $xml */
182
183 return $xml;
184 }
185
186 /**
187 * Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
188 *
189 * @param string $filestream
190 *
191 * @return string
192 */
193 public function scanFile($filestream)
194 {
195 return $this->scan(file_get_contents($filestream));
196 }
197 }
198