ILIAS  trunk Revision v12.0_alpha-1227-g7ff6d300864
SVGBlacklistPreProcessor.php
Go to the documentation of this file.
1<?php
2
20
24
33{
34 use IsMimeTypeOrExtension;
35
39 private const SVG_MIME_TYPE = 'image/svg+xml';
43 private const REGEX_SCRIPT = '/<script/m';
47 private const REGEX_FOREIGN_OBJECT = '/<foreignObject/m';
51 private const REGEX_BASE64 = '/data:.*;base64/m';
55 private const SVG = 'svg';
56 private string $rejection_message = 'The SVG file contains possibily malicious code.';
61 private string $ok_message = 'SVG OK';
65 private array $svg_event_lists = [
66 "onbegin",
67 "onend",
68 "onrepeat",
69 "onabort",
70 "onerror",
71 "onresize",
72 "onscroll",
73 "onunload",
74 "onabort",
75 "onerror",
76 "onresize",
77 "onscroll",
78 "onunload",
79 "oncancel",
80 "oncanplay",
81 "oncanplaythrough",
82 "onchange",
83 "onclick",
84 "onclose",
85 "oncuechange",
86 "ondblclick",
87 "ondrag",
88 "ondragend",
89 "ondragenter",
90 "ondragleave",
91 "ondragover",
92 "ondragstart",
93 "ondrop",
94 "ondurationchange",
95 "onemptied",
96 "onended",
97 "onerror",
98 "onfocus",
99 "oninput",
100 "oninvalid",
101 "onkeydown",
102 "onkeypress",
103 "onkeyup",
104 "onload",
105 "onloadeddata",
106 "onloadedmetadata",
107 "onloadstart",
108 "onmousedown",
109 "onmouseenter",
110 "onmouseleave",
111 "onmousemove",
112 "onmouseout",
113 "onmouseover",
114 "onmouseup",
115 "onmousewheel",
116 "onpause",
117 "onplay",
118 "onplaying",
119 "onprogress",
120 "onratechange",
121 "onreset",
122 "onresize",
123 "onscroll",
124 "onseeked",
125 "onseeking",
126 "onselect",
127 "onshow",
128 "onstalled",
129 "onsubmit",
130 "onsuspend",
131 "ontimeupdate",
132 "ontoggle",
133 "onvolumechange",
134 "onwaiting",
135 "onactivate",
136 "onfocusin",
137 "onfocusout"
138 ];
139
140 public function __construct(
141 ?string $rejection_message = null,
142 ?string $additional_message_script = null,
143 ?string $additional_message_base64 = null,
144 ?string $additional_message_foreign_object = null,
145 ?string $additional_message_elements = null
146 ) {
147 $this->rejection_message = $rejection_message ?? $this->rejection_message;
148 $this->rejection_message_script = $additional_message_script ?? 'contains script tags';
149 $this->rejection_message_base64 = $additional_message_base64 ?? 'contains base64 encoded content';
150 $this->rejection_message_foreign_object = $additional_message_foreign_object ?? 'contains foreign object';
151 $this->rejection_message_elements = $additional_message_elements ?? 'contains not allowed or unknown elements or attributes';
152 }
153
154 private function isSVG(Metadata $metadata): bool
155 {
156 return $this->isMimeTypeOrExtension(
157 $metadata,
158 self::SVG,
159 [self::SVG_MIME_TYPE]
160 );
161 }
162
163 public function process(FileStream $stream, Metadata $metadata): ProcessingStatus
164 {
165 if ($this->isSVG($metadata) && !$this->checkStream($stream)) {
166 return new ProcessingStatus(ProcessingStatus::DENIED, $this->rejection_message);
167 }
168 return new ProcessingStatus(ProcessingStatus::OK, $this->ok_message);
169 }
170
171 public function getDomDocument(string $raw_svg_content): ?\DOMDocument
172 {
173 $dom = new \DOMDocument();
174 try {
175 $dom->loadXML($raw_svg_content, LIBXML_NOWARNING | LIBXML_NOERROR);
176 } catch (\Exception) {
177 return null;
178 }
179 $errors = libxml_get_errors();
180 if ($errors !== []) {
181 return null;
182 }
183 return $dom;
184 }
185
186 protected function checkStream(FileStream $stream): bool
187 {
188 $raw_svg_content = (string) $stream;
189 if (false === $raw_svg_content) {
190 return false;
191 }
192
193 // Check for script tags directly
194 if ($this->hasContentScriptTag($raw_svg_content)) {
195 $this->rejection_message = $this->rejection_message;
196 return false;
197 }
198
199 // Analyze the SVG
200 $dom = $this->getDomDocument($raw_svg_content);
201 if ($dom === null) {
202 return false;
203 }
204
205 // loop through all attributes of elements recursively and check for event attributes
206 $looper = $this->getDOMAttributesLooper();
207 $prohibited_attributes = (fn(string $name): bool => in_array(strtolower($name), $this->svg_event_lists, true));
208 return $looper($dom, $prohibited_attributes) !== false;
209 }
210
211 private function hasContentScriptTag(string $raw_svg_content): bool
212 {
213 // Check for Base64 encoded Content
214 if (preg_match(self::REGEX_BASE64, $raw_svg_content)) {
215 $this->rejection_message .= ' ' . $this->rejection_message_base64;
216 return true;
217 }
218
219 // Check for script tags directly
220 if (preg_match(self::REGEX_SCRIPT, $raw_svg_content)) {
221 $this->rejection_message .= ' ' . $this->rejection_message_script;
222 return true;
223 }
224
225 // Check for script tags directly
226 if (preg_match(self::REGEX_FOREIGN_OBJECT, $raw_svg_content)) {
227 $this->rejection_message .= ' ' . $this->rejection_message_foreign_object;
228 return true;
229 }
230
231 return false;
232 }
233
234 protected function getDOMAttributesLooper(): \Closure
235 {
236 return function (\DOMDocument $dom, \Closure $closure): bool {
237 $attributes_looper = function (\DOMNode $node, \Closure $closure) use (&$attributes_looper): bool {
238 foreach ($node->attributes as $attribute) {
239 if ($closure($attribute->name)) {
240 $this->rejection_message .= sprintf(
241 $this->rejection_message_elements . ' (%s)',
242 $attribute->name
243 );
244 return false;
245 }
246 }
247 foreach ($node->childNodes as $child) {
248 if ($child instanceof \DOMElement && !$attributes_looper($child, $closure)) {
249 return false;
250 }
251 }
252 return true;
253 };
254 foreach ($dom->getElementsByTagName("*") as $i => $element) {
255 if ($attributes_looper($element, $closure) === false) {
256 return false;
257 }
258 }
259 return true;
260 };
261 }
262}
__construct(?string $rejection_message=null, ?string $additional_message_script=null, ?string $additional_message_base64=null, ?string $additional_message_foreign_object=null, ?string $additional_message_elements=null)
process(FileStream $stream, Metadata $metadata)
This method gets invoked by the file upload service to process the file with the help of the processo...
The base interface for all filesystem streams.
Definition: FileStream.php:32
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...