VYPR
High severityNVD Advisory· Published Aug 6, 2018· Updated Aug 5, 2024

CVE-2018-14716

CVE-2018-14716

Description

SEOmatic plugin before 3.1.4 for Craft CMS contains a Server-Side Template Injection vulnerability via unauthenticated requests that generate canonical URLs, allowing execution of Twig code.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

SEOmatic plugin before 3.1.4 for Craft CMS contains a Server-Side Template Injection vulnerability via unauthenticated requests that generate canonical URLs, allowing execution of Twig code.

Vulnerability

The SEOmatic plugin for Craft CMS versions before 3.1.4 suffers from a Server-Side Template Injection (SSTI) vulnerability. When a request does not match any element, the plugin incorrectly generates a canonical URL using user-supplied input without proper sanitization. This allows an attacker to inject Twig template code into the URL, which is then executed by the template engine [1][2][4].

Exploitation

An unauthenticated attacker can trigger the vulnerability by sending a crafted HTTP request to the Craft CMS site with a malicious URI containing Twig code. The injection is reflected in the Link header of the response. However, since control characters are escaped, the attacker must use methods like craft.request.getUserAgent() to pass payload data via headers such as the User-Agent. For example, the request path can include Twig code that reads the User-Agent header and uses craft.config.get() to extract sensitive configuration values [1][4].

Impact

Successful exploitation allows an attacker to execute arbitrary Twig template code on the server. This can lead to information disclosure, such as reading database passwords, and potentially full compromise of the Craft CMS instance. The attacker does not require authentication [1][4].

Mitigation

The vulnerability is fixed in version 3.1.4 of the SEOmatic plugin [2][3]. Users should update to this version or later. No other workarounds are mentioned in the available references. The plugin is not listed on CISA's Known Exploited Vulnerabilities (KEV) catalog.

AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nystudio107/craft-seomaticPackagist
< 3.1.43.1.4

Affected products

1

Patches

2
8689e12017ee

Merge branch 'release/3.1.4' into v3

https://github.com/nystudio107/craft-seomaticAndrew WelchJul 23, 2018via osv
7 files changed · +78 31
  • CHANGELOG.md+4 0 modified
    @@ -1,5 +1,9 @@
     # SEOmatic Changelog
     
    +## 3.1.4 - 2018.07.23
    +### Changed
    +* Changed the way requests that don't match any elements generate the `canonicalUrl`, to avoid potentially executing injected Twig code
    +
     ## 3.1.3 - 2018.07.20
     ### Added
     * Added **Additional Sitemap URLs** to Site Settings -> Miscellaneous for custom sitemap URLs 
    
  • composer.json+1 1 modified
    @@ -2,7 +2,7 @@
         "name": "nystudio107/craft-seomatic",
         "description": "SEOmatic facilitates modern SEO best practices & implementation for Craft CMS 3. It is a turnkey SEO system that is comprehensive, powerful, and flexible.",
         "type": "craft-plugin",
    -    "version": "3.1.3",
    +    "version": "3.1.4",
         "keywords": [
             "craft",
             "cms",
    
  • README.md+1 1 modified
    @@ -622,7 +622,7 @@ The `seomatic.meta` variable contains all of the meta variables that control the
     * **`seomatic.meta.seoImageWidth`** - the width of the SEO image
     * **`seomatic.meta.seoImageHeight`** - the height of the SEO image
     * **`seomatic.meta.seoImageDescription`** - a textual description of the SEO image
    -* **`seomatic.meta.canonicalUrl`** - the URL used for the `<link rel="canonical">` tag. By default, this is set to `{{ craft.app.request.pathInfo | striptags }}` or `{entry.url}`/`{category.url}`/`{product.url}`, but you can change it as you see fit. This variable is also used to set the `link rel="canonical"` HTTP header.
    +* **`seomatic.meta.canonicalUrl`** - the URL used for the `<link rel="canonical">` tag. By default, this is set to `{seomatic.helper.safeCanonicalUrl()}` or `{entry.url}`/`{category.url}`/`{product.url}`, but you can change it as you see fit. This variable is also used to set the `link rel="canonical"` HTTP header.
     * **`seomatic.meta.robots`** - the setting used for the `<meta name="robots">` tag that controls how bots should index your website. This variable is also used to set the `X-Robots-Tag` HTTP header. [Learn More](https://developers.google.com/search/reference/robots_meta_tag)
     
     ##### Facebook OpenGraph Variables:
    
  • src/helpers/DynamicMeta.php+6 0 modified
    @@ -500,6 +500,12 @@ public static function getLocalizedUrls(string $uri = null, int $siteId = null):
                         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);
    
  • src/models/MetaGlobalVars.php+14 0 modified
    @@ -199,6 +199,20 @@ public function __construct(array $config = [])
             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
          */
    
  • src/seomatic-config/globalmeta/GlobalVars.php+1 1 modified
    @@ -26,7 +26,7 @@
             'seoImageWidth'           => '',
             'seoImageHeight'          => '',
             'seoImageDescription'     => '',
    -        'canonicalUrl'            => '{{ craft.app.request.pathInfo | striptags }}',
    +        'canonicalUrl'            => '{seomatic.helper.safeCanonicalUrl()}',
             'robots'                  => 'all',
             'ogType'                  => 'website',
             'ogTitle'                 => '{seomatic.meta.seoTitle}',
    
  • src/services/Helper.php+51 28 modified
    @@ -11,20 +11,23 @@
     
     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
    @@ -38,9 +41,28 @@ 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
          */
    @@ -86,8 +108,8 @@ public static function truncateOnWord($string, $length, $substring = '…'): str
          * 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
    @@ -131,6 +153,7 @@ public static function seoFileLink($url, $robots = '', $canonical = '', $inline
                 .$inlineStr
                 .'/'
                 .$fileName;
    +
             return Template::raw(UrlHelper::siteUrl($seoFileLink));
         }
     
    @@ -238,6 +261,30 @@ 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
    @@ -282,28 +329,4 @@ public function socialTransformHeight($asset, string $transformName = '', $siteI
         {
             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);
    -    }
     }
    
1e7d1d084ac3

Changed the way requests that don't match any elements generate the `canonicalUrl`, to avoid potentially executing injected Twig code

https://github.com/nystudio107/craft-seomaticAndrew WelchJul 23, 2018via ghsa
4 files changed · +72 29
  • src/helpers/DynamicMeta.php+6 0 modified
    @@ -500,6 +500,12 @@ public static function getLocalizedUrls(string $uri = null, int $siteId = null):
                         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);
    
  • src/models/MetaGlobalVars.php+14 0 modified
    @@ -199,6 +199,20 @@ public function __construct(array $config = [])
             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
          */
    
  • src/seomatic-config/globalmeta/GlobalVars.php+1 1 modified
    @@ -26,7 +26,7 @@
             'seoImageWidth'           => '',
             'seoImageHeight'          => '',
             'seoImageDescription'     => '',
    -        'canonicalUrl'            => '{{ craft.app.request.pathInfo | striptags }}',
    +        'canonicalUrl'            => '{seomatic.helper.safeCanonicalUrl()}',
             'robots'                  => 'all',
             'ogType'                  => 'website',
             'ogTitle'                 => '{seomatic.meta.seoTitle}',
    
  • src/services/Helper.php+51 28 modified
    @@ -11,20 +11,23 @@
     
     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
    @@ -38,9 +41,28 @@ 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
          */
    @@ -86,8 +108,8 @@ public static function truncateOnWord($string, $length, $substring = '…'): str
          * 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
    @@ -131,6 +153,7 @@ public static function seoFileLink($url, $robots = '', $canonical = '', $inline
                 .$inlineStr
                 .'/'
                 .$fileName;
    +
             return Template::raw(UrlHelper::siteUrl($seoFileLink));
         }
     
    @@ -238,6 +261,30 @@ 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
    @@ -282,28 +329,4 @@ public function socialTransformHeight($asset, string $transformName = '', $siteI
         {
             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);
    -    }
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.