Craft affected by IDOR via GraphQL @parseRefs
Description
Craft is a content management system (CMS). Prior to 4.17.0-beta.1 and 5.9.0-beta.1, the GraphQL directive @parseRefs, intended to parse internal reference tags (e.g., {user:1:email}), can be abused by both authenticated users and unauthenticated guests (if a Public Schema is enabled) to access sensitive attributes of any element in the CMS. The implementation in Elements::parseRefs fails to perform authorization checks, allowing attackers to read data they are not authorized to view. This vulnerability is fixed in 4.17.0-beta.1 and 5.9.0-beta.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Craft CMS GraphQL @parseRefs directive lacks authorization checks, allowing attackers to read sensitive attributes of any element.
Vulnerability
Overview
CVE-2026-28696 is an authorization bypass vulnerability in Craft CMS's GraphQL @parseRefs directive. The Elements::parseRefs method, which resolves internal reference tags (e.g., {user:1:email}), fails to perform canView() authorization checks before fetching and returning element attributes [1][2]. This allows both authenticated users and unauthenticated guests (if a Public Schema is enabled) to access sensitive data from any element type, including entries, assets, users, and categories [2].
Exploitation
Details
An attacker can exploit this by crafting GraphQL queries that include @parseRefs directives with reference tags targeting arbitrary elements. The vulnerability is polymorphic: getElementTypeByRefHandle allows referencing any element type, and because Craft elements use __get() to resolve custom field handles, attackers are not limited to core attributes [2]. For example, a payload like {user:1:email} can leak user email addresses, while {entry:456:myConfidentialField} can exfiltrate custom field data from private entries. Additionally, accessing methods that trigger errors (e.g., {user:1:authKey}) can expose full server stack traces in GraphQL error responses, revealing internal paths [2].
Impact
Successful exploitation leads to unauthorized disclosure of sensitive information, including user email addresses, usernames, admin status, custom field data, and internal file system paths. This can facilitate privilege escalation, user enumeration, and further attacks against the CMS [2]. The vulnerability is particularly severe when a Public Schema is enabled, as it allows unauthenticated exploitation [1][2].
Mitigation
The vulnerability is fixed in Craft CMS versions 4.17.0-beta.1 and 5.9.0-beta.1 [1][2]. Users should upgrade immediately. The fix makes the @parseRefs directive optional and likely introduces proper authorization checks [4]. No workarounds are documented; disabling the Public Schema may reduce the attack surface but does not fully mitigate the risk for authenticated users.
AI Insight generated on May 18, 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 |
|---|---|---|
craftcms/cmsPackagist | >= 4.0.0-RC1, < 4.17.0-beta.1 | 4.17.0-beta.1 |
craftcms/cmsPackagist | >= 5.0.0-RC1, < 5.9.0-beta.1 | 5.9.0-beta.1 |
Affected products
2Patches
14d98a07e4758Make `@parseRefs` optional
7 files changed · +68 −6
CHANGELOG-WIP.md+1 −0 modified@@ -9,6 +9,7 @@ ### Development - Added support for referencing environment variables anywhere within settings that support them (e.g. `foo/$ENV_NAME/bar` or `foo-${ENV_NAME}-bar`). ([#17949](https://github.com/craftcms/cms/pull/17949)) - Added the `uuid()` Twig function. +- The `@parseRefs` GraphQL directive is now optional for each GraphQL schema. (GHSA-7x43-mpfg-r9wj) ### Extensibility - Added `craft\services\Search::deleteOrphanedIndexJobs()`.
src/config/app.php+1 −1 modified@@ -4,7 +4,7 @@ 'id' => 'CraftCMS', 'name' => 'Craft CMS', 'version' => '4.16.17', - 'schemaVersion' => '4.17.0.0', + 'schemaVersion' => '4.17.0.1', 'minVersionRequired' => '3.7.11', 'basePath' => dirname(__DIR__), // Defines the @app alias 'runtimePath' => '@storage/runtime', // Defines the @runtime alias
src/helpers/Gql.php+2 −0 modified@@ -352,6 +352,8 @@ public static function createFullAccessSchema(): GqlSchema $traverser($group); } + $schema->scope[] = 'directive:parseRefs'; + return $schema; }
src/migrations/m260105_191335_parseRefs_schema_component.php+40 −0 added@@ -0,0 +1,40 @@ +<?php + +namespace craft\migrations; + +use Craft; +use craft\db\Migration; + +/** + * m260105_191335_parseRefs_schema_component migration. + */ +class m260105_191335_parseRefs_schema_component extends Migration +{ + /** + * @inheritdoc + */ + public function safeUp(): bool + { + $gql = Craft::$app->getGql(); + $schemas = $gql->getSchemas(); + + foreach ($schemas as $schema) { + if (in_array('directive:parseRefs', $schema->scope)) { + continue; + } + + $schema->scope[] = 'directive:parseRefs'; + $gql->saveSchema($schema); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + return true; + } +}
src/services/Gql.php+7 −3 modified@@ -375,7 +375,7 @@ public function getSchemaDef(?GqlSchema $schema = null, bool $prebuildSchema = f 'typeLoader' => TypeLoader::class . '::loadType', 'query' => TypeLoader::loadType('Query'), 'mutation' => TypeLoader::loadType('Mutation'), - 'directives' => $this->_loadGqlDirectives(), + 'directives' => $this->_loadGqlDirectives($schema), ]; // If we're not required to pre-build the schema the relevant GraphQL types will be added to the Schema @@ -1391,21 +1391,25 @@ private function _registerGqlMutations(): void /** * Get GraphQL query definitions * + * @param GqlSchema|null $schema * @return GqlDirective[] */ - private function _loadGqlDirectives(): array + private function _loadGqlDirectives(?GqlSchema $schema): array { /** @var class-string<Directive>[] $directiveClasses */ $directiveClasses = [ // Directives FormatDateTime::class, Markdown::class, Money::class, - ParseRefs::class, StripTags::class, Trim::class, ]; + if (in_array('directive:parseRefs', $schema->scope)) { + $directiveClasses[] = ParseRefs::class; + } + if (!Craft::$app->getConfig()->getGeneral()->disableGraphqlTransformDirective) { $directiveClasses[] = Transform::class; }
src/templates/graphql/schemas/_edit.twig+15 −2 modified@@ -35,7 +35,7 @@ <li> {{ checkbox({ - label: props.label, + label: raw(props.label|md(inlineOnly=true, encode=true)), name: 'permissions[]', value: permissionName, checked: checked, @@ -101,7 +101,7 @@ </div> {% endfor %} - <hr/> + <hr> <h2>{{ 'Choose the available mutations for this schema:'|t('app') }}</h2> {% for category, catPermissions in schemaComponents.mutations|filter %} @@ -113,6 +113,19 @@ </div> {% endfor %} + <hr> + <h2>{{ 'Choose optional features available to this schema:'|t('app') }}</h2> + + <div class="user-permissions"> + {{ permissionList(schema, { + 'directive:parseRefs': { + label: '{name} directive'|t('app', { + name: '`@parseRefs`', + }), + warning: 'Provides read-only access to user data and most content.', + }, + }) }} + </div> {% endblock %}
src/translations/en/app.php+2 −0 modified@@ -1154,6 +1154,7 @@ 'Propagating {type}' => 'Propagating {type}', 'Propagation Key Format' => 'Propagation Key Format', 'Propagation Method' => 'Propagation Method', + 'Provides read-only access to user data and most content.' => 'Provides read-only access to user data and most content.', 'Province' => 'Province', 'Pruning extra revisions' => 'Pruning extra revisions', 'Public Schema' => 'Public Schema', @@ -1977,6 +1978,7 @@ '{first}-{last} of {total}' => '{first}–{last} of {total}', '{names} {total, plural, =1{is installed as a trial} other{are installed as trials}}.' => '{names} {total, plural, =1{is installed as a trial} other{are installed as trials}}.', '{name} active, more info' => '{name} active, more info', + '{name} directive' => '{name} directive', '{name} folder' => '{name} folder', '{name} has been added, but an error occurred when installing it.' => '{name} has been added, but an error occurred when installing it.', '{name} is licensed for the {licenseEdition} edition, but the {currentEdition} edition is installed.' => '{name} is licensed for the {licenseEdition} edition, but the {currentEdition} edition is installed.',
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-7x43-mpfg-r9wjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28696ghsaADVISORY
- github.com/craftcms/cms/commit/4d98a07e47580f1712095825d3e3c4d67bc9f8b9ghsax_refsource_MISCWEB
- github.com/craftcms/cms/security/advisories/GHSA-7x43-mpfg-r9wjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.