Craft CMS GraphQL entry mutation resolvers (saveEntry, deleteEntry) read siteId directly from $arguments without passing through ArgumentManager::prepareArguments(), which is the function that enforces site-scope filtering via array_intersect against the GraphQL schema’s allowed sites. The query path (ElementResolver::prepareElementQuery) correctly calls prepareArguments(), so queries to unauthorized sites return empty. But mutations bypass this entirely — an attacker with a token scoped to Site A can create, modify, or delete entries in Site B by passing siteId in the mutations argument.
Impact
- Confidentiality:
saveEntry loads the element by siteId — attacker can read content from unauthorized sites.
- Integrity:
saveEntry writes to the specified site’s elements_sites record; propagation spreads changes to all sites.
- Availability:
deleteEntry locates the element by siteId then calls deleteElementById without siteId, deleting the element across all sites.
Report ID: 1540
Craft CMS GraphQL entry mutation resolvers (
saveEntry,deleteEntry) readsiteIddirectly from$argumentswithout passing throughArgumentManager::prepareArguments(), which is the function that enforces site-scope filtering viaarray_intersectagainst the GraphQL schema’s allowed sites. The query path (ElementResolver::prepareElementQuery) correctly callsprepareArguments(), so queries to unauthorized sites return empty. But mutations bypass this entirely — an attacker with a token scoped to Site A can create, modify, or delete entries in Site B by passingsiteIdin the mutations argument.Impact
saveEntryloads the element bysiteId— attacker can read content from unauthorized sites.saveEntrywrites to the specified site’selements_sitesrecord; propagation spreads changes to all sites.deleteEntrylocates the element bysiteIdthen callsdeleteElementByIdwithoutsiteId, deleting the element across all sites.Report ID: 1540