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