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.
| Package | Affected versions | Patched versions |
|---|---|---|
nystudio107/craft-seomaticPackagist | < 3.1.4 | 3.1.4 |
Affected products
1Patches
28689e12017eeMerge branch 'release/3.1.4' into v3
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); - } }
1e7d1d084ac3Changed the way requests that don't match any elements generate the `canonicalUrl`, to avoid potentially executing injected Twig code
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- www.exploit-db.com/exploits/45108/mitreexploitx_refsource_EXPLOIT-DB
- github.com/advisories/GHSA-6j9m-rp7m-3gfgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-14716ghsaADVISORY
- ha.cker.info/exploitation-of-server-side-template-injection-with-craft-cms-plguin-seomaticghsaWEB
- ha.cker.info/exploitation-of-server-side-template-injection-with-craft-cms-plguin-seomatic/mitrex_refsource_MISC
- github.com/nystudio107/craft-seomatic/commit/1e7d1d084ac3a89e7ec70620f2749110508d1ce1ghsax_refsource_CONFIRMWEB
- github.com/nystudio107/craft-seomatic/releases/tag/3.1.4ghsax_refsource_CONFIRMWEB
- twitter.com/nystudio107/status/1021847835418009605ghsax_refsource_CONFIRMWEB
- twitter.com/nystudio107/status/1021855169515057152ghsax_refsource_CONFIRMWEB
- www.exploit-db.com/exploits/45108ghsaWEB
News mentions
0No linked articles in our index yet.