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