Merge "Don't try to verify XML well-formedness for partial SVG uploads"
[lhc/web/wiklou.git] / includes / upload / UploadBase.php
index 5de543e..a278652 100644 (file)
@@ -69,8 +69,6 @@ abstract class UploadBase {
        const WINDOWS_NONASCII_FILENAME = 13;
        const FILENAME_TOO_LONG = 14;
 
-       const SESSION_STATUS_KEY = 'wsUploadStatusData';
-
        /**
         * @param int $error
         * @return string
@@ -426,7 +424,7 @@ abstract class UploadBase {
         * @return mixed True of the file is verified, array otherwise.
         */
        protected function verifyFile() {
-               global $wgVerifyMimeType;
+               global $wgVerifyMimeType, $wgDisableUploadScriptChecks;
                wfProfileIn( __METHOD__ );
 
                $status = $this->verifyPartialFile();
@@ -448,6 +446,18 @@ abstract class UploadBase {
                        }
                }
 
+               # check for htmlish code and javascript
+               if ( !$wgDisableUploadScriptChecks ) {
+                       if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
+                               $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
+                               if ( $svgStatus !== false ) {
+                                       wfProfileOut( __METHOD__ );
+
+                                       return $svgStatus;
+                               }
+                       }
+               }
+
                $handler = MediaHandler::getHandler( $mime );
                if ( $handler ) {
                        $handlerStatus = $handler->verifyUpload( $this->mTempPath );
@@ -506,7 +516,7 @@ abstract class UploadBase {
                                return array( 'uploadscripted' );
                        }
                        if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
-                               $svgStatus = $this->detectScriptInSvg( $this->mTempPath );
+                               $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
                                if ( $svgStatus !== false ) {
                                        wfProfileOut( __METHOD__ );
 
@@ -746,6 +756,8 @@ abstract class UploadBase {
                                );
                        }
                        wfRunHooks( 'UploadComplete', array( &$this ) );
+
+                       $this->postProcessUpload();
                }
 
                wfProfileOut( __METHOD__ );
@@ -753,6 +765,35 @@ abstract class UploadBase {
                return $status;
        }
 
+       /**
+        * Perform extra steps after a successful upload.
+        *
+        * @since  1.25
+        */
+       public function postProcessUpload() {
+               global $wgUploadThumbnailRenderMap;
+
+               $jobs = array();
+
+               $sizes = $wgUploadThumbnailRenderMap;
+               rsort( $sizes );
+
+               $file = $this->getLocalFile();
+
+               foreach ( $sizes as $size ) {
+                       if ( $file->isVectorized()
+                               || $file->getWidth() > $size ) {
+                                       $jobs[] = new ThumbnailRenderJob( $file->getTitle(), array(
+                                               'transformParams' => array( 'width' => $size ),
+                                       ) );
+                       }
+               }
+
+               if ( $jobs ) {
+                       JobQueueGroup::singleton()->push( $jobs );
+               }
+       }
+
        /**
         * Returns the title of the file to be uploaded. Sets mTitleError in case
         * the name was illegal.
@@ -1252,9 +1293,10 @@ abstract class UploadBase {
 
        /**
         * @param string $filename
+        * @param bool $partial
         * @return mixed False of the file is verified (does not contain scripts), array otherwise.
         */
-       protected function detectScriptInSvg( $filename ) {
+       protected function detectScriptInSvg( $filename, $partial ) {
                $this->mSVGNSError = false;
                $check = new XmlTypeCheck(
                        $filename,
@@ -1264,7 +1306,8 @@ abstract class UploadBase {
                );
                if ( $check->wellFormed !== true ) {
                        // Invalid xml (bug 58553)
-                       return array( 'uploadinvalidxml' );
+                       // But only when non-partial (bug 65724)
+                       return $partial ? false : array( 'uploadinvalidxml' );
                } elseif ( $check->filterMatch ) {
                        if ( $this->mSVGNSError ) {
                                return array( 'uploadscriptednamespace', $this->mSVGNSError );
@@ -1297,7 +1340,8 @@ abstract class UploadBase {
         * @param array $attribs
         * @return bool
         */
-       public function checkSvgScriptCallback( $element, $attribs ) {
+       public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
+
                list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element );
 
                // We specifically don't include:
@@ -1381,6 +1425,14 @@ abstract class UploadBase {
                        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 );
@@ -1423,6 +1475,18 @@ abstract class UploadBase {
                                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'
@@ -1463,23 +1527,23 @@ abstract class UploadBase {
                        }
 
                        # use CSS styles to bring in remote code
-                       # catch url("http:..., url('http:..., url(http:..., but not url("#..., url('#..., url(#....
-                       $tagsList = "font|clip-path|fill|filter|marker|marker-end|marker-mid|marker-start|mask|stroke";
                        if ( $stripped == 'style'
-                               && preg_match_all(
-                                       '!((?:' . $tagsList . ')\s*:\s*url\s*\(\s*["\']?\s*[^#]+.*?\))!sim',
-                                       $value,
-                                       $matches
-                               )
+                               && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
                        ) {
-                               foreach ( $matches[1] as $match ) {
-                                       if ( !preg_match( '!(?:' . $tagsList . ')\s*:\s*url\s*\(\s*(#|\'#|"#)!sim', $match ) ) {
-                                               wfDebug( __METHOD__ . ": Found svg setting a style with "
-                                                       . "remote url '$attrib'='$value' in uploaded file.\n" );
+                               wfDebug( __METHOD__ . ": Found svg setting a style with "
+                                       . "remote url '$attrib'='$value' in uploaded file.\n" );
+                               return true;
+                       }
 
-                                               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
@@ -1497,6 +1561,58 @@ abstract class UploadBase {
                return false; //No scripts detected
        }
 
+       /**
+        * Check a block of CSS or CSS fragment for anything that looks like
+        * it is bringing in remote code.
+        * @param string $value a string of CSS
+        * @param bool $propOnly only check css properties (start regex with :)
+        * @return bool true if the CSS contains an illegal string, false if otherwise
+        */
+       private static function checkCssFragment( $value ) {
+
+               # Forbid external stylesheets, for both reliability and to protect viewer's privacy
+               if ( strpos( $value, '@import' ) !== false ) {
+                       return true;
+               }
+
+               # We allow @font-face to embed fonts with data: urls, so we snip the string
+               # 'url' out so this case won't match when we check for urls below
+               $pattern = '!(@font-face\s*{[^}]*src:)url(\("data:;base64,)!im';
+               $value = preg_replace( $pattern, '$1$2', $value );
+
+               # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
+               # properties filter and accelerator don't seem to be useful for xss in SVG files.
+               # Expression and -o-link don't seem to work either, but filtering them here in case.
+               # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
+               # but not local ones such as url("#..., url('#..., url(#....
+               if ( preg_match( '!expression
+                               | -o-link\s*:
+                               | -o-link-source\s*:
+                               | -o-replace\s*:!imx', $value ) ) {
+                       return true;
+               }
+
+               if ( preg_match_all(
+                               "!(\s*(url|image|image-set)\s*\(\s*[\"']?\s*[^#]+.*?\))!sim",
+                               $value,
+                               $matches
+                       ) !== 0
+               ) {
+                       # TODO: redo this in one regex. Until then, url("#whatever") matches the first
+                       foreach ( $matches[1] as $match ) {
+                               if ( !preg_match( "!\s*(url|image|image-set)\s*\(\s*(#|'#|\"#)!im", $match ) ) {
+                                       return true;
+                               }
+                       }
+               }
+
+               if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
+                       return true;
+               }
+
+               return false;
+       }
+
        /**
         * Divide the element name passed by the xml parser to the callback into URI and prifix.
         * @param string $element
@@ -1882,29 +1998,38 @@ abstract class UploadBase {
        }
 
        /**
-        * Get the current status of a chunked upload (used for polling).
-        * The status will be read from the *current* user session.
+        * Get the current status of a chunked upload (used for polling)
+        *
+        * The value will be read from cache.
+        *
+        * @param User $user
         * @param string $statusKey
         * @return Status[]|bool
         */
-       public static function getSessionStatus( $statusKey ) {
-               return isset( $_SESSION[self::SESSION_STATUS_KEY][$statusKey] )
-                       ? $_SESSION[self::SESSION_STATUS_KEY][$statusKey]
-                       : false;
+       public static function getSessionStatus( User $user, $statusKey ) {
+               $key = wfMemcKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey );
+
+               return wfGetCache( CACHE_ANYTHING )->get( $key );
        }
 
        /**
-        * Set the current status of a chunked upload (used for polling).
-        * The status will be stored in the *current* user session.
+        * Set the current status of a chunked upload (used for polling)
+        *
+        * The value will be set in cache for 1 day
+        *
+        * @param User $user
         * @param string $statusKey
         * @param array|bool $value
         * @return void
         */
-       public static function setSessionStatus( $statusKey, $value ) {
+       public static function setSessionStatus( User $user, $statusKey, $value ) {
+               $key = wfMemcKey( 'uploadstatus', $user->getId() ?: md5( $user->getName() ), $statusKey );
+
+               $cache = wfGetCache( CACHE_ANYTHING );
                if ( $value === false ) {
-                       unset( $_SESSION[self::SESSION_STATUS_KEY][$statusKey] );
+                       $cache->delete( $key );
                } else {
-                       $_SESSION[self::SESSION_STATUS_KEY][$statusKey] = $value;
+                       $cache->set( $key, $value, 86400 );
                }
        }
 }