ILIAS  release_7 Revision v7.30-3-g800a261c036
All Data Structures Namespaces Files Functions Variables Modules Pages
SVGBlacklistPreProcessor.php
Go to the documentation of this file.
1 <?php
2 
20 
24 
32 final class SVGBlacklistPreProcessor implements PreProcessor
33 {
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 }
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
if($format !==null) $name
Definition: metadata.php:230
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const DENIED
Upload got denied by a processor, the upload will be removed immediately.
Interface FileStream The base interface for all filesystem streams.
Definition: FileStream.php:17
$i
Definition: metadata.php:24