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