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