CVE-2018-14716: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: A Server Side Template Injection (SSTI) was discovered in the SEOmatic plugin before 3.1.4 for Craft CMS, because requests that don't match any elements incorrectly generate the canonicalUrl, and can lead to execution of Twig code.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 7.5
Published: 2018-08-06T20:29Z
Repository: https://github.com/nystudio107/craft-seomatic
Language: PHP
Files changed: src/helpers/DynamicMeta.php, src/models/MetaGlobalVars.php, src/seomatic-config/globalmeta/GlobalVars.php, src/services/Helper.php
Commit date: 2018-07-23 18:19:43 -0400
Commit message: Changed the way requests that don't match any elements generate the `canonicalUrl`, to avoid potentially executing injected Twig code

Signed-off-by: Andrew Welch <andrew@nystudio107.com>

Diff: diff --git a/src/helpers/DynamicMeta.php b/src/helpers/DynamicMeta.php
index dec9b9c3..955106d4 100644
--- a/src/helpers/DynamicMeta.php
+++ b/src/helpers/DynamicMeta.php
@@ -498,10 +498,16 @@ class DynamicMeta
                 } catch (Exception $e) {
                     $url = '';
                     Craft::error($e->getMessage(), __METHOD__);
                 }
             }
+            // Strip any query string params, and make sure we have an absolute URL with protocol
+            if ($urlParams === null) {
+                $url = UrlHelper::stripQueryString($url);
+            }
+            $url = UrlHelper::absoluteUrlWithProtocol($url);
+
             $url = $url ?? '';
             $language = $site->language;
             $ogLanguage = str_replace('-', '_', $language);
             $hreflangLanguage = $language;
             $hreflangLanguage = strtolower($hreflangLanguage);
diff --git a/src/models/MetaGlobalVars.php b/src/models/MetaGlobalVars.php
index ea17fc2d..209ca4ed 100644
--- a/src/models/MetaGlobalVars.php
+++ b/src/models/MetaGlobalVars.php
@@ -197,10 +197,24 @@ class MetaGlobalVars extends VarsModel
         // Unset any deprecated properties
         //unset($config['siteNamePosition']);
         parent::__construct($config);
     }
 
+    /**
+     * @inheritdoc
+     */
+    public function init()
+    {
+        parent::init();
+        // If we have potentially unsafe Twig code, strip it out
+        if (!empty($this->canonicalUrl)) {
+            if (strpos($this->canonicalUrl, 'craft.app.request.pathInfo') !== false) {
+                $this->canonicalUrl = '{seomatic.helper.safeCanonicalUrl()}';
+            }
+        }
+    }
+
     /**
      * @inheritdoc
      */
     public function rules(): array
     {
diff --git a/src/seomatic-config/globalmeta/GlobalVars.php b/src/seomatic-config/globalmeta/GlobalVars.php
index 01f20771..e16a5430 100644
--- a/src/seomatic-config/globalmeta/GlobalVars.php
+++ b/src/seomatic-config/globalmeta/GlobalVars.php
@@ -24,11 +24,11 @@ return [
         'seoKeywords'             => '',
         'seoImage'                => '',
         'seoImageWidth'           => '',
         'seoImageHeight'          => '',
         'seoImageDescription'     => '',
-        'canonicalUrl'            => '{{ craft.app.request.pathInfo | striptags }}',
+        'canonicalUrl'            => '{seomatic.helper.safeCanonicalUrl()}',
         'robots'                  => 'all',
         'ogType'                  => 'website',
         'ogTitle'                 => '{seomatic.meta.seoTitle}',
         'ogSiteNamePosition'      => 'none',
         'ogDescription'           => '{seomatic.meta.seoDescription}',
diff --git a/src/services/Helper.php b/src/services/Helper.php
index b6896cd7..1aa10a80 100644
--- a/src/services/Helper.php
+++ b/src/services/Helper.php
@@ -9,24 +9,27 @@
  * @copyright Copyright (c) 2017 nystudio107
  */
 
 namespace nystudio107\seomatic\services;
 
+use nystudio107\seomatic\helpers\UrlHelper;
 use nystudio107\seomatic\Seomatic;
 use nystudio107\seomatic\helpers\DynamicMeta as DynamicMetaHelper;
 use nystudio107\seomatic\helpers\ImageTransform as ImageTransformHelper;
 use nystudio107\seomatic\helpers\Schema as SchemaHelper;
 use nystudio107\seomatic\helpers\Text as TextHelper;
 
+use Craft;
 use craft\base\Component;
 use craft\elements\Asset;
 use craft\elements\db\MatrixBlockQuery;
 use craft\elements\db\TagQuery;
 use craft\helpers\Template;
-use craft\helpers\UrlHelper;
 use craft\web\twig\variables\Paginate;
 
+use yii\base\InvalidConfigException;
+
 /**
  * @author    nystudio107
  * @package   Seomatic
  * @since     3.0.0
  */
@@ -36,13 +39,32 @@ class Helper extends Component
     // =========================================================================
 
     // Public Methods
     // =========================================================================
 
+    /**
+     * Return the canonical URL for the request, with the query string stripped
+     *
+     * @return string
+     */
+    public static function safeCanonicalUrl(): string
+    {
+        $url = '';
+        try {
+            $url = Craft::$app->getRequest()->getPathInfo();
+        } catch (InvalidConfigException $e) {
+            Craft::error($e->getMessage(), __METHOD__);
+        }
+        $url = UrlHelper::stripQueryString($url);
+
+        return UrlHelper::absoluteUrlWithProtocol($url);
+    }
+
     /**
      * Paginate based on the passed in Paginate variable as returned from the
-     * Twig {% paginate %} tag: https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
+     * Twig {% paginate %} tag:
+     * https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
      *
      * @param Paginate $pageInfo
      */
     public static function paginate(Paginate $pageInfo)
     {
@@ -84,12 +106,12 @@ class Helper extends Component
 
     /**
      * Return a list of localized URLs that are in the current site's group
      * The current URI is used if $uri is null. Similarly, the current site is
      * used if $siteId is null.
-     * The resulting array of arrays has `id`, `language`, `ogLanguage`, `hreflangLanguage`,
-     * and `url` as keys.
+     * The resulting array of arrays has `id`, `language`, `ogLanguage`,
+     * `hreflangLanguage`, and `url` as keys.
      *
      * @param string|null $uri
      * @param int|null    $siteId
      *
      * @return array
@@ -129,10 +151,11 @@ class Helper extends Component
             .base64_encode($canonical)
             .'/'
             .$inlineStr
             .'/'
             .$fileName;
+
         return Template::raw(UrlHelper::siteUrl($seoFileLink));
     }
 
     /**
      * Load the appropriate meta containers for the given $uri and optional
@@ -236,10 +259,34 @@ class Helper extends Component
     public static function extractSummary($text = '', $useStopWords = true): string
     {
         return TextHelper::extractSummary($text, $useStopWords);
     }
 
+    /**
+     * Return a flattened, indented menu of the given $path
+     *
+     * @param string $path
+     *
+     * @return array
+     */
+    public static function getTypeMenu($path): array
+    {
+        return SchemaHelper::getTypeMenu($path);
+    }
+
+    /**
+     * Return a single menu of schemas starting at $path
+     *
+     * @param string $path
+     *
+     * @return array
+     */
+    public static function getSingleTypeMenu($path): array
+    {
+        return SchemaHelper::getSingleTypeMenu($path);
+    }
+
     /**
      * Transform the $asset for social media sites in $transformName and
      * optional $siteId
      *
      * @param int|Asset $asset         the Asset or Asset ID
@@ -280,30 +327,6 @@ class Helper extends Component
      */
     public function socialTransformHeight($asset, string $transformName = '', $siteId = null): string
     {
         return ImageTransformHelper::socialTransformHeight($asset, $transformName, $siteId);
     }
-
-    /**
-     * Return a flattened, indented menu of the given $path
-     *
-     * @param string $path
-     *
-     * @return array
-     */
-    public static function getTypeMenu($path): array
-    {
-        return SchemaHelper::getTypeMenu($path);
-    }
-
-    /**
-     * Return a single menu of schemas starting at $path
-     *
-     * @param string $path
-     *
-     * @return array
-     */
-    public static function getSingleTypeMenu($path): array
-    {
-        return SchemaHelper::getSingleTypeMenu($path);
-    }
 }

Vulnerable code:         'canonicalUrl'            => '{{ craft.app.request.pathInfo | striptags }}',
use craft\helpers\UrlHelper;
     * Twig {% paginate %} tag: https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
     * The resulting array of arrays has `id`, `language`, `ogLanguage`, `hreflangLanguage`,
     * and `url` as keys.

    /**
     * Return a flattened, indented menu of the given $path
     *
     * @param string $path
     *
     * @return array
     */
    public static function getTypeMenu($path): array
    {
        return SchemaHelper::getTypeMenu($path);
    }

    /**
     * Return a single menu of schemas starting at $path
     *
     * @param string $path
     *
     * @return array
     */
    public static function getSingleTypeMenu($path): array
    {
        return SchemaHelper::getSingleTypeMenu($path);
    }
Fixed code:             // Strip any query string params, and make sure we have an absolute URL with protocol
            if ($urlParams === null) {
                $url = UrlHelper::stripQueryString($url);
            }
            $url = UrlHelper::absoluteUrlWithProtocol($url);

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        // If we have potentially unsafe Twig code, strip it out
        if (!empty($this->canonicalUrl)) {
            if (strpos($this->canonicalUrl, 'craft.app.request.pathInfo') !== false) {
                $this->canonicalUrl = '{seomatic.helper.safeCanonicalUrl()}';
            }
        }
    }

        'canonicalUrl'            => '{seomatic.helper.safeCanonicalUrl()}',
use nystudio107\seomatic\helpers\UrlHelper;
use Craft;
use yii\base\InvalidConfigException;

    /**
     * Return the canonical URL for the request, with the query string stripped
     *
     * @return string
     */
    public static function safeCanonicalUrl(): string
    {
        $url = '';
        try {
            $url = Craft::$app->getRequest()->getPathInfo();
        } catch (InvalidConfigException $e) {
            Craft::error($e->getMessage(), __METHOD__);
        }
        $url = UrlHelper::stripQueryString($url);

        return UrlHelper::absoluteUrlWithProtocol($url);
    }

     * Twig {% paginate %} tag:
     * https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
     * The resulting array of arrays has `id`, `language`, `ogLanguage`,
     * `hreflangLanguage`, and `url` as keys.

    /**
     * Return a flattened, indented menu of the given $path
     *
     * @param string $path
     *
     * @return array
     */
    public static function getTypeMenu($path): array
    {
        return SchemaHelper::getTypeMenu($path);
    }

    /**
     * Return a single menu of schemas starting at $path
     *
     * @param string $path
     *
     * @return array
     */
    public static function getSingleTypeMenu($path): array
    {
        return SchemaHelper::getSingleTypeMenu($path);
    }