ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
SVGBlacklistPreProcessor.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
27use ILIAS\FileUpload\Processor\IsMimeTypeOrExtension;
28
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
42 private string $rejection_message = 'The SVG file contains possibily malicious code.';
47 private string $ok_message = 'SVG OK';
48
52 private array $svg_event_lists = [
53 "onbegin", "onend", "onrepeat", "onabort", "onerror", "onresize", "onscroll", "onunload",
54 "oncancel", "oncanplay", "oncanplaythrough", "onchange", "onclick", "onclose",
55 "oncuechange", "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave",
56 "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied", "onended",
57 "onfocus", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", "onload",
58 "onloadeddata", "onloadedmetadata", "onloadstart", "onmousedown", "onmouseenter",
59 "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel",
60 "onpause", "onplay", "onplaying", "onprogress", "onratechange", "onreset", "onresize",
61 "onscroll", "onseeked", "onseeking", "onselect", "onshow", "onstalled", "onsubmit",
62 "onsuspend", "ontimeupdate", "ontoggle", "onvolumechange", "onwaiting",
63 "onactivate", "onfocusin", "onfocusout",
64 ];
65
66 public function __construct(
67 ?string $rejection_message = null,
68 ?string $additional_message_script = null,
69 ?string $additional_message_base64 = null,
70 ?string $additional_message_foreign_object = null,
71 ?string $additional_message_elements = null
72 ) {
73 $this->rejection_message = $rejection_message ?? $this->rejection_message;
74 $this->rejection_message_script = $additional_message_script ?? 'contains script tags';
75 $this->rejection_message_base64 = $additional_message_base64 ?? 'contains base64 encoded content';
76 $this->rejection_message_foreign_object = $additional_message_foreign_object ?? 'contains foreign object';
77 $this->rejection_message_elements = $additional_message_elements ?? 'contains not allowed or unknown elements or attributes';
78 }
79
80 private function isSVG(Metadata $metadata): bool
81 {
82 return $this->isMimeTypeOrExtension($metadata, self::SVG, [self::SVG_MIME_TYPE]);
83 }
84
85 public function process(FileStream $stream, Metadata $metadata): ProcessingStatus
86 {
87 if ($this->isSVG($metadata) && !$this->checkStream($stream)) {
88 return new ProcessingStatus(ProcessingStatus::DENIED, $this->rejection_message);
89 }
90 return new ProcessingStatus(ProcessingStatus::OK, $this->ok_message);
91 }
92
93 public function getDomDocument(string $raw_svg_content): ?\DOMDocument
94 {
95 $dom = new \DOMDocument();
96 try {
97 $dom->loadXML($raw_svg_content, LIBXML_NOWARNING | LIBXML_NOERROR);
98 } catch (\Exception) {
99 return null;
100 }
101 $errors = libxml_get_errors();
102 if ($errors !== []) {
103 return null;
104 }
105 return $dom;
106 }
107
108 protected function checkStream(FileStream $stream): bool
109 {
110 $raw_svg_content = (string) $stream;
111 if ($raw_svg_content === false) {
112 return false;
113 }
114 if ($this->hasContentScriptTag($raw_svg_content)) {
115 return false;
116 }
117 $dom = $this->getDomDocument($raw_svg_content);
118 if ($dom === null) {
119 return false;
120 }
121 $looper = $this->getDOMAttributesLooper();
122 $prohibited_attributes = fn(string $name): bool => in_array(strtolower($name), $this->svg_event_lists, true);
123 return $looper($dom, $prohibited_attributes) !== false;
124 }
125
126 private function hasContentScriptTag(string $raw_svg_content): bool
127 {
128 if (preg_match(self::REGEX_BASE64, $raw_svg_content)) {
129 $this->rejection_message .= ' ' . $this->rejection_message_base64;
130 return true;
131 }
132 if (preg_match(self::REGEX_SCRIPT, $raw_svg_content)) {
133 $this->rejection_message .= ' ' . $this->rejection_message_script;
134 return true;
135 }
136 if (preg_match(self::REGEX_FOREIGN_OBJECT, $raw_svg_content)) {
137 $this->rejection_message .= ' ' . $this->rejection_message_foreign_object;
138 return true;
139 }
140 return false;
141 }
142
143 protected function getDOMAttributesLooper(): \Closure
144 {
145 return function (\DOMDocument $dom, \Closure $closure): bool {
146 $attributes_looper = function (\DOMNode $node, \Closure $closure) use (&$attributes_looper): bool {
147 foreach ($node->attributes as $attribute) {
148 if ($closure($attribute->name)) {
149 $this->rejection_message .= sprintf(
150 $this->rejection_message_elements . ' (%s)',
151 $attribute->name
152 );
153 return false;
154 }
155 }
156 foreach ($node->childNodes as $child) {
157 if ($child instanceof \DOMElement && !$attributes_looper($child, $closure)) {
158 return false;
159 }
160 }
161 return true;
162 };
163 foreach ($dom->getElementsByTagName("*") as $element) {
164 if ($attributes_looper($element, $closure) === false) {
165 return false;
166 }
167 }
168 return true;
169 };
170 }
171}
Data transfer object that carries the raw SVG data as a string, created from trusted sources.
Definition: SVG.php:30
PreProcessor which checks SVGs for scripts and other malicious content.
__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...
The base interface for all filesystem streams.
Definition: FileStream.php:32