/** * @todo Replace this with a whitelist filter! * @param $element string * @param $attribs array * @return bool */ public function checkSvgScriptCallback($element, $attribs, $data = null) { list($namespace, $strippedElement) = $this->splitXmlNamespace($element); static $validNamespaces = array('', 'adobe:ns:meta/', 'http://creativecommons.org/ns#', 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd', 'http://ns.adobe.com/adobeillustrator/10.0/', 'http://ns.adobe.com/adobesvgviewerextensions/3.0/', 'http://ns.adobe.com/extensibility/1.0/', 'http://ns.adobe.com/flows/1.0/', 'http://ns.adobe.com/illustrator/1.0/', 'http://ns.adobe.com/imagereplacement/1.0/', 'http://ns.adobe.com/pdf/1.3/', 'http://ns.adobe.com/photoshop/1.0/', 'http://ns.adobe.com/saveforweb/1.0/', 'http://ns.adobe.com/variables/1.0/', 'http://ns.adobe.com/xap/1.0/', 'http://ns.adobe.com/xap/1.0/g/', 'http://ns.adobe.com/xap/1.0/g/img/', 'http://ns.adobe.com/xap/1.0/mm/', 'http://ns.adobe.com/xap/1.0/rights/', 'http://ns.adobe.com/xap/1.0/stype/dimensions#', 'http://ns.adobe.com/xap/1.0/stype/font#', 'http://ns.adobe.com/xap/1.0/stype/manifestitem#', 'http://ns.adobe.com/xap/1.0/stype/resourceevent#', 'http://ns.adobe.com/xap/1.0/stype/resourceref#', 'http://ns.adobe.com/xap/1.0/t/pg/', 'http://purl.org/dc/elements/1.1/', 'http://purl.org/dc/elements/1.1', 'http://schemas.microsoft.com/visio/2003/svgextensions/', 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd', 'http://web.resource.org/cc/', 'http://www.freesoftware.fsf.org/bkchem/cdml', 'http://www.inkscape.org/namespaces/inkscape', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'http://www.w3.org/2000/svg'); if (!in_array($namespace, $validNamespaces)) { wfDebug(__METHOD__ . ": Non-svg namespace '{$namespace}' in uploaded file.\n"); // @TODO return a status object to a closure in XmlTypeCheck, for MW1.21+ $this->mSVGNSError = $namespace; return true; } /* * check for elements that can contain javascript */ if ($strippedElement == 'script') { wfDebug(__METHOD__ . ": Found script element '{$element}' in uploaded file.\n"); return true; } # e.g., <svg xmlns="http://www.w3.org/2000/svg"> <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg> if ($strippedElement == 'handler') { wfDebug(__METHOD__ . ": Found scriptable element '{$element}' in uploaded file.\n"); return true; } # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block if ($strippedElement == 'stylesheet') { wfDebug(__METHOD__ . ": Found scriptable element '{$element}' in uploaded file.\n"); return true; } # Block iframes, in case they pass the namespace check if ($strippedElement == 'iframe') { wfDebug(__METHOD__ . ": iframe in uploaded file.\n"); return true; } # Check <style> css if ($strippedElement == 'style' && self::checkCssFragment(Sanitizer::normalizeCss($data))) { wfDebug(__METHOD__ . ": hostile css in style element.\n"); return true; } foreach ($attribs as $attrib => $value) { $stripped = $this->stripXmlNamespace($attrib); $value = strtolower($value); if (substr($stripped, 0, 2) == 'on') { wfDebug(__METHOD__ . ": Found event-handler attribute '{$attrib}'='{$value}' in uploaded file.\n"); return true; } # href with non-local target (don't allow http://, javascript:, etc) if ($stripped == 'href' && strpos($value, 'data:') !== 0 && strpos($value, '#') !== 0) { if (!($strippedElement === 'a' && preg_match('!^https?://!im', $value))) { wfDebug(__METHOD__ . ": Found href attribute <{$strippedElement} " . "'{$attrib}'='{$value}' in uploaded file.\n"); return true; } } # href with embedded svg as target if ($stripped == 'href' && preg_match('!data:[^,]*image/svg[^,]*,!sim', $value)) { wfDebug(__METHOD__ . ": Found href to embedded svg \"<{$strippedElement} '{$attrib}'='{$value}'...\" in uploaded file.\n"); return true; } # href with embedded (text/xml) svg as target if ($stripped == 'href' && preg_match('!data:[^,]*text/xml[^,]*,!sim', $value)) { wfDebug(__METHOD__ . ": Found href to embedded svg \"<{$strippedElement} '{$attrib}'='{$value}'...\" in uploaded file.\n"); return true; } # Change href with animate from (http://html5sec.org/#137). This doesn't seem # possible without embedding the svg, but filter here in case. if ($stripped == 'from' && $strippedElement === 'animate' && !preg_match('!^https?://!im', $value)) { wfDebug(__METHOD__ . ": Found animate that might be changing href using from " . "\"<{$strippedElement} '{$attrib}'='{$value}'...\" in uploaded file.\n"); return true; } # use set/animate to add event-handler attribute to parent if (($strippedElement == 'set' || $strippedElement == 'animate') && $stripped == 'attributename' && substr($value, 0, 2) == 'on') { wfDebug(__METHOD__ . ": Found svg setting event-handler attribute with \"<{$strippedElement} {$stripped}='{$value}'...\" in uploaded file.\n"); return true; } # use set to add href attribute to parent element if ($strippedElement == 'set' && $stripped == 'attributename' && strpos($value, 'href') !== false) { wfDebug(__METHOD__ . ": Found svg setting href attribute '{$value}' in uploaded file.\n"); return true; } # use set to add a remote / data / script target to an element if ($strippedElement == 'set' && $stripped == 'to' && preg_match('!(http|https|data|script):!sim', $value)) { wfDebug(__METHOD__ . ": Found svg setting attribute to '{$value}' in uploaded file.\n"); return true; } # use handler attribute with remote / data / script if ($stripped == 'handler' && preg_match('!(http|https|data|script):!sim', $value)) { wfDebug(__METHOD__ . ": Found svg setting handler with remote/data/script '{$attrib}'='{$value}' in uploaded file.\n"); return true; } # use CSS styles to bring in remote code if ($stripped == 'style' && self::checkCssFragment(Sanitizer::normalizeCss($value))) { wfDebug(__METHOD__ . ": Found svg setting a style with " . "remote url '{$attrib}'='{$value}' in uploaded file.\n"); return true; } # Several attributes can include css, css character escaping isn't allowed $cssAttrs = array('font', 'clip-path', 'fill', 'filter', 'marker', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'); if (in_array($stripped, $cssAttrs) && self::checkCssFragment($value)) { wfDebug(__METHOD__ . ": Found svg setting a style with " . "remote url '{$attrib}'='{$value}' in uploaded file.\n"); return true; } # image filters can pull in url, which could be svg that executes scripts if ($strippedElement == 'image' && $stripped == 'filter' && preg_match('!url\\s*\\(!sim', $value)) { wfDebug(__METHOD__ . ": Found image filter with url: \"<{$strippedElement} {$stripped}='{$value}'...\" in uploaded file.\n"); return true; } } return false; //No scripts detected }
/** * @todo Replace this with a whitelist filter! * @param string $element * @param array $attribs * @return bool */ public function checkSvgScriptCallback($element, $attribs, $data = null) { list($namespace, $strippedElement) = $this->splitXmlNamespace($element); // We specifically don't include: // http://www.w3.org/1999/xhtml (bug 60771) static $validNamespaces = ['', 'adobe:ns:meta/', 'http://creativecommons.org/ns#', 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd', 'http://ns.adobe.com/adobeillustrator/10.0/', 'http://ns.adobe.com/adobesvgviewerextensions/3.0/', 'http://ns.adobe.com/extensibility/1.0/', 'http://ns.adobe.com/flows/1.0/', 'http://ns.adobe.com/illustrator/1.0/', 'http://ns.adobe.com/imagereplacement/1.0/', 'http://ns.adobe.com/pdf/1.3/', 'http://ns.adobe.com/photoshop/1.0/', 'http://ns.adobe.com/saveforweb/1.0/', 'http://ns.adobe.com/variables/1.0/', 'http://ns.adobe.com/xap/1.0/', 'http://ns.adobe.com/xap/1.0/g/', 'http://ns.adobe.com/xap/1.0/g/img/', 'http://ns.adobe.com/xap/1.0/mm/', 'http://ns.adobe.com/xap/1.0/rights/', 'http://ns.adobe.com/xap/1.0/stype/dimensions#', 'http://ns.adobe.com/xap/1.0/stype/font#', 'http://ns.adobe.com/xap/1.0/stype/manifestitem#', 'http://ns.adobe.com/xap/1.0/stype/resourceevent#', 'http://ns.adobe.com/xap/1.0/stype/resourceref#', 'http://ns.adobe.com/xap/1.0/t/pg/', 'http://purl.org/dc/elements/1.1/', 'http://purl.org/dc/elements/1.1', 'http://schemas.microsoft.com/visio/2003/svgextensions/', 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd', 'http://taptrix.com/inkpad/svg_extensions', 'http://web.resource.org/cc/', 'http://www.freesoftware.fsf.org/bkchem/cdml', 'http://www.inkscape.org/namespaces/inkscape', 'http://www.opengis.net/gml', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'http://www.w3.org/2000/svg', 'http://www.w3.org/tr/rec-rdf-syntax/']; // Inkscape mangles namespace definitions created by Adobe Illustrator. // This is nasty but harmless. (T144827) $isBuggyInkscape = preg_match('/^&(#38;)*ns_[a-z_]+;$/', $namespace); if (!($isBuggyInkscape || in_array($namespace, $validNamespaces))) { wfDebug(__METHOD__ . ": Non-svg namespace '{$namespace}' in uploaded file.\n"); /** @todo Return a status object to a closure in XmlTypeCheck, for MW1.21+ */ $this->mSVGNSError = $namespace; return true; } /* * check for elements that can contain javascript */ if ($strippedElement == 'script') { wfDebug(__METHOD__ . ": Found script element '{$element}' in uploaded file.\n"); return ['uploaded-script-svg', $strippedElement]; } # e.g., <svg xmlns="http://www.w3.org/2000/svg"> # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg> if ($strippedElement == 'handler') { wfDebug(__METHOD__ . ": Found scriptable element '{$element}' in uploaded file.\n"); return ['uploaded-script-svg', $strippedElement]; } # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block if ($strippedElement == 'stylesheet') { wfDebug(__METHOD__ . ": Found scriptable element '{$element}' in uploaded file.\n"); return ['uploaded-script-svg', $strippedElement]; } # Block iframes, in case they pass the namespace check if ($strippedElement == 'iframe') { wfDebug(__METHOD__ . ": iframe in uploaded file.\n"); return ['uploaded-script-svg', $strippedElement]; } # Check <style> css if ($strippedElement == 'style' && self::checkCssFragment(Sanitizer::normalizeCss($data))) { wfDebug(__METHOD__ . ": hostile css in style element.\n"); return ['uploaded-hostile-svg']; } foreach ($attribs as $attrib => $value) { $stripped = $this->stripXmlNamespace($attrib); $value = strtolower($value); if (substr($stripped, 0, 2) == 'on') { wfDebug(__METHOD__ . ": Found event-handler attribute '{$attrib}'='{$value}' in uploaded file.\n"); return ['uploaded-event-handler-on-svg', $attrib, $value]; } # Do not allow relative links, or unsafe url schemas. # For <a> tags, only data:, http: and https: and same-document # fragment links are allowed. For all other tags, only data: # and fragment are allowed. if ($stripped == 'href' && strpos($value, 'data:') !== 0 && strpos($value, '#') !== 0) { if (!($strippedElement === 'a' && preg_match('!^https?://!i', $value))) { wfDebug(__METHOD__ . ": Found href attribute <{$strippedElement} " . "'{$attrib}'='{$value}' in uploaded file.\n"); return ['uploaded-href-attribute-svg', $strippedElement, $attrib, $value]; } } # only allow data: targets that should be safe. This prevents vectors like, # image/svg, text/xml, application/xml, and text/html, which can contain scripts if ($stripped == 'href' && strncasecmp('data:', $value, 5) === 0) { // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*. // @codingStandardsIgnoreStart Generic.Files.LineLength $parameters = '(?>;[a-zA-Z0-9\\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\\!#$&\'*+.^_`{|}~-]+|"(?>[\\0-\\x0c\\x0e-\\x21\\x23-\\x5b\\x5d-\\x7f]+|\\\\[\\0-\\x7f])*"))*(?:;base64)?'; // @codingStandardsIgnoreEnd if (!preg_match("!^data:\\s*image/(gif|jpeg|jpg|png){$parameters},!i", $value)) { wfDebug(__METHOD__ . ": Found href to unwhitelisted data: uri " . "\"<{$strippedElement} '{$attrib}'='{$value}'...\" in uploaded file.\n"); return ['uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value]; } } # Change href with animate from (http://html5sec.org/#137). if ($stripped === 'attributename' && $strippedElement === 'animate' && $this->stripXmlNamespace($value) == 'href') { wfDebug(__METHOD__ . ": Found animate that might be changing href using from " . "\"<{$strippedElement} '{$attrib}'='{$value}'...\" in uploaded file.\n"); return ['uploaded-animate-svg', $strippedElement, $attrib, $value]; } # use set/animate to add event-handler attribute to parent if (($strippedElement == 'set' || $strippedElement == 'animate') && $stripped == 'attributename' && substr($value, 0, 2) == 'on') { wfDebug(__METHOD__ . ": Found svg setting event-handler attribute with " . "\"<{$strippedElement} {$stripped}='{$value}'...\" in uploaded file.\n"); return ['uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value]; } # use set to add href attribute to parent element if ($strippedElement == 'set' && $stripped == 'attributename' && strpos($value, 'href') !== false) { wfDebug(__METHOD__ . ": Found svg setting href attribute '{$value}' in uploaded file.\n"); return ['uploaded-setting-href-svg']; } # use set to add a remote / data / script target to an element if ($strippedElement == 'set' && $stripped == 'to' && preg_match('!(http|https|data|script):!sim', $value)) { wfDebug(__METHOD__ . ": Found svg setting attribute to '{$value}' in uploaded file.\n"); return ['uploaded-wrong-setting-svg', $value]; } # use handler attribute with remote / data / script if ($stripped == 'handler' && preg_match('!(http|https|data|script):!sim', $value)) { wfDebug(__METHOD__ . ": Found svg setting handler with remote/data/script " . "'{$attrib}'='{$value}' in uploaded file.\n"); return ['uploaded-setting-handler-svg', $attrib, $value]; } # use CSS styles to bring in remote code if ($stripped == 'style' && self::checkCssFragment(Sanitizer::normalizeCss($value))) { wfDebug(__METHOD__ . ": Found svg setting a style with " . "remote url '{$attrib}'='{$value}' in uploaded file.\n"); return ['uploaded-remote-url-svg', $attrib, $value]; } # Several attributes can include css, css character escaping isn't allowed $cssAttrs = ['font', 'clip-path', 'fill', 'filter', 'marker', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke']; if (in_array($stripped, $cssAttrs) && self::checkCssFragment($value)) { wfDebug(__METHOD__ . ": Found svg setting a style with " . "remote url '{$attrib}'='{$value}' in uploaded file.\n"); return ['uploaded-remote-url-svg', $attrib, $value]; } # image filters can pull in url, which could be svg that executes scripts if ($strippedElement == 'image' && $stripped == 'filter' && preg_match('!url\\s*\\(!sim', $value)) { wfDebug(__METHOD__ . ": Found image filter with url: " . "\"<{$strippedElement} {$stripped}='{$value}'...\" in uploaded file.\n"); return ['uploaded-image-filter-svg', $strippedElement, $stripped, $value]; } } return false; // No scripts detected }