CVE-2022-0970: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository getgrav/grav prior to 1.7.31.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2022-03-15T17:15Z
Repository: https://github.com/getgrav/grav
Language: PHP
Files changed: CHANGELOG.md, system/src/Grav/Common/Media/Traits/MediaUploadTrait.php, system/src/Grav/Common/Security.php, system/src/Grav/Framework/Form/FormFlashFile.php
Commit date: 2022-03-02 13:37:51 +0200
Commit message: Added XSS check for uploaded SVG files before they get stored

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b4711cfc6..28c2db4557 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,11 @@
 # v1.7.31
 ## mm/dd/2022
 
 1. [](#new)
     * Added support to get image size for SVG vector images [#3533](https://github.com/getgrav/grav/pull/3533)
+    * Added XSS check for uploaded SVG files before they get stored
     * Fixed phpstan issues (All level 2, Framework level 5)
 2. [](#bugfix)
     * Fixed `'mbstring' extension is not loaded` error, use Polyfill instead [#3504](https://github.com/getgrav/grav/pull/3504)
     * Fixed new `Utils::pathinfo()` and `Utils::basename()` being too strict for legacy use [#3542](https://github.com/getgrav/grav/issues/3542)
     * Fixed non-standard video html atributes generated by `{{ media.html() }}` [#3540](https://github.com/getgrav/grav/issues/3540)
diff --git a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php
index 36a4503f1b..88591f6aeb 100644
--- a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php
+++ b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php
@@ -98,10 +98,14 @@ trait MediaUploadTrait
             'filename' => $uploadedFile->getClientFilename(),
             'mime' => $uploadedFile->getClientMediaType(),
             'size' => $uploadedFile->getSize(),
         ];
 
+        if ($uploadedFile instanceof FormFlashFile) {
+            $uploadedFile->checkXss();
+        }
+
         return $this->checkFileMetadata($metadata, $filename, $settings);
     }
 
     /**
      * Checks that file metadata meets the requirements. Returns new filename.
diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php
index 01ea0f13c8..779e61918a 100644
--- a/system/src/Grav/Common/Security.php
+++ b/system/src/Grav/Common/Security.php
@@ -23,10 +23,26 @@ use function is_string;
  * Class Security
  * @package Grav\Common
  */
 class Security
 {
+    /**
+     * @param string $filepath
+     * @param array|null $options
+     * @return string|null
+     */
+    public static function detectXssFromSvgFile(string $filepath, array $options = null): ?string
+    {
+        if (file_exists($filepath) && Grav::instance()['config']->get('security.sanitize_svg')) {
+            $content = file_get_contents($filepath);
+
+            return static::detectXss($content, $options);
+        }
+
+        return null;
+    }
+
     /**
      * Sanitize SVG string for XSS code
      *
      * @param string $svg
      * @return string
@@ -198,11 +214,11 @@ class Security
         $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', static function ($m) {
             return chr(hexdec($m[2]));
         }, $string);
 
         // Clean up entities
-        $string = preg_replace('!(&#[0-9]+)!u', '$1;', $string);
+        $string = preg_replace('!(&#[0-9]+);?!u', '$1;', $string);
 
         // Decode entities
         $string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8');
 
         // Strip whitespace characters
diff --git a/system/src/Grav/Framework/Form/FormFlashFile.php b/system/src/Grav/Framework/Form/FormFlashFile.php
index 6c995993eb..65af544d1b 100644
--- a/system/src/Grav/Framework/Form/FormFlashFile.php
+++ b/system/src/Grav/Framework/Form/FormFlashFile.php
@@ -7,10 +7,12 @@
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Framework\Form;
 
+use Grav\Common\Security;
+use Grav\Common\Utils;
 use Grav\Framework\Psr7\Stream;
 use InvalidArgumentException;
 use JsonSerializable;
 use Psr\Http\Message\StreamInterface;
 use Psr\Http\Message\UploadedFileInterface;
@@ -180,10 +182,25 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
     public function jsonSerialize()
     {
         return $this->upload;
     }
 
+    /**
+     * @return void
+     */
+    public function checkXss(): void
+    {
+        $tmpFile = $this->getTmpFile();
+        $mime = $this->getClientMediaType();
+        if (Utils::contains($mime, 'svg', false)) {
+            $response = Security::detectXssFromSvgFile($tmpFile);
+            if ($response) {
+                throw new RuntimeException(sprintf('SVG file XSS check failed on %s', $response));
+            }
+        }
+    }
+
     /**
      * @return string|null
      */
     public function getTmpFile(): ?string
     {

Vulnerable code:         $string = preg_replace('!(&#[0-9]+)!u', '$1;', $string);
Fixed code:     * Added XSS check for uploaded SVG files before they get stored
        if ($uploadedFile instanceof FormFlashFile) {
            $uploadedFile->checkXss();
        }

    /**
     * @param string $filepath
     * @param array|null $options
     * @return string|null
     */
    public static function detectXssFromSvgFile(string $filepath, array $options = null): ?string
    {
        if (file_exists($filepath) && Grav::instance()['config']->get('security.sanitize_svg')) {
            $content = file_get_contents($filepath);

            return static::detectXss($content, $options);
        }

        return null;
    }

        $string = preg_replace('!(&#[0-9]+);?!u', '$1;', $string);
use Grav\Common\Security;
use Grav\Common\Utils;
    /**
     * @return void
     */
    public function checkXss(): void
    {
        $tmpFile = $this->getTmpFile();
        $mime = $this->getClientMediaType();
        if (Utils::contains($mime, 'svg', false)) {
            $response = Security::detectXssFromSvgFile($tmpFile);
            if ($response) {
                throw new RuntimeException(sprintf('SVG file XSS check failed on %s', $response));
            }
        }
    }