Craft's GraphQL 'Sites scope' had two separate holes in it — and both needed zero credentials
I run Craft headless more than once, which means GraphQL schema scoping is doing real access-control work for me, not just shaping a response. So two advisories Craft published on 23 October — both patched silently a month earlier in 5.11.2, disclosed with technical detail only now — caught my attention for being the same category of mistake twice over: the Sites scope on a schema is supposed to be a hard boundary, and it had two independent, unauthenticated ways around it.
Bug one: just don't pass the site argument
GHSA-mp59-9rf5-93wr (CWE-862, Missing Authorization, Moderate) affects 5.0.0 up to before 5.11.2. The setup: a multi-site install, a GraphQL schema whose Sites scope includes only site 1, and a section shared across sites that the schema is allowed to query. Querying the localized element field with an explicit siteId for the out-of-scope site correctly returns nothing — the advisory confirms this with a control test using a token that legitimately has access to both sites, so the denial is real Sites-scope enforcement, not missing data. But omitting the argument entirely returns the out-of-scope site's content anyway. The check existed; it just wasn't applied to the default path. A single unauthenticated POST to /actions/graphql/api, no login, no non-default config — just leaving an argument out.
Bug two: walk prev/next until you've read everything
The second, GHSA-xpg7-m6gm-4xfh, same severity and affected range, is structurally different: an anonymous client who can read one visible entry's ID can chain next{next{next{...}}} through a section's `prev`/`next` fields and surface entries from a section the schema explicitly excludes — as long as the excluded section shares an entry type with an in-scope one, so its GraphQL type exists in the schema and the response serializes. The payoff is real content: title, slug, UID, post date, full custom-field data. On a headless or partially-exposed install, the advisory notes the schema scope is the only thing standing between the public and that content — and here, chaining two fields was enough to walk past it and enumerate the entire entry list.
The gap that's worth noticing on its own
Both were fixed in 5.11.2 on 17 September — the changelog entry just says "Fixed several security issues: ... two moderate authorization bypasses ..." — but the advisories with actual technical detail weren't published until 23 October, over a month later. That's a deliberate and reasonable pattern (patch quietly first, disclose with specifics once the fix has had time to roll out), but it means anyone cross-referencing "did this affect me" against the changelog alone, rather than watching the advisories directly, had no way to know what the September bump actually closed until five weeks after the fact.
What I checked on my own installs
Every Craft project I run headless is confirmed on 5.11.2 or later. Beyond the version check, the actual lesson here is about what "the schema only returns what's in scope" means in practice: it means that for every field capable of returning related or adjacent content — not just the ones you query directly, but anything reachable through traversal like `prev`/`next` — until you've specifically checked it. A scoping boundary that holds for direct queries but not for a chained field isn't a scoping boundary, it's a boundary with a gap you haven't found yet.