While exploring a web app’s GraphQL API, I came across something interesting — a function that didn’t just leak information, but actually let me join private organizations without any approval. What started as simple curiosity turned into a full-blown account and data breach scenario.
Background
The application in question allows teams and organizations to collaborate privately within workspaces. Users can be invited to organizations, and permissions determine what each user can access — so, ideally, no outsider should ever see or touch internal data.
But as it turns out, the GraphQL API had a completely different idea.
The Discovery
I began by browsing through the app’s front-end source files and noticed a few GraphQL operations that weren’t directly referenced in the UI. That’s always worth checking.
Two stood out immediately:
joinOrganizationByEmailDomain(mutation)GetPublicOrganizationsByEmailDomain(query)
They sounded harmless — but putting them together revealed something dangerous.
Step 1 — Leaking Organization IDs
First, the following GraphQL query could be sent with nothing more than a company’s email domain:
[
{
"operationName": "GetPublicOrganizationsByEmailDomain",
"variables": {"emailDomain": "targetcompany.com"},
"query": "query GetPublicOrganizationsByEmailDomain($emailDomain: String!)
{ publicOrganizationsByEmailDomain(emailDomain: $emailDomain)
{ _id members { user { email } } name } }"
}
]
The response contained a full list of organizations using that domain — including their organization IDs, names, and member email addresses.
At this point, I could already enumerate users and internal team structures without ever logging in.
Step 2 — Joining Any Organization
Next, I tried the joinOrganizationByEmailDomain mutation:
[
{
"operationName": "joinOrganizationByEmailDomain",
"variables": {"organizationId": "<OrganizationID>"},
"query": "mutation joinOrganizationByEmailDomain($organizationId: ID!)
{ joinOrganizationByEmailDomain(organizationId: $organizationId)
{ _id __typename } }"
}
]
Replacing <OrganizationID> with the ID from the previous query granted instant membership into the target organization — no approval, no invitation, no checks.
Just like that, I was inside.
Step 3 — Data Exfiltration
Once added to the organization, all internal resources became accessible: design files, project assets, and shared documents.
Even worse, this could be chained with another vulnerability I had previously reported — one that allowed exporting organization data to a separate workspace under my control.
That meant I could:
- Access and copy proprietary assets
- Transfer them to my own organization
- Fully exfiltrate sensitive information
Essentially, a total data breach.
The Impact
This wasn’t just a minor privacy bug. It was a complete breakdown of the organization access model.
With a single unauthenticated query, I could:
- Enumerate company members and structure
- Join any private organization
- Access and export confidential data
The implications were serious: intellectual property theft, business espionage, and reputational damage — all possible through two GraphQL endpoints.
Root Cause
There were two core problems:
- Overly permissive data exposure —
GetPublicOrganizationsByEmailDomainleaked internal identifiers and member details. - Missing authorization controls —
joinOrganizationByEmailDomaintrusted client-side logic and didn’t verify the legitimacy of join requests.
Combined, these turned a basic information disclosure into full unauthorized access.
Recommendations
To fix this, I recommended:
- Restricting
GetPublicOrganizationsByEmailDomainto authenticated and domain-verified users. - Adding proper authorization checks to
joinOrganizationByEmailDomain. - Avoiding exposure of organization IDs and member lists through public queries.
- Performing a broader audit for insecure internal mutations.
Final Thoughts
What I love about GraphQL is also what makes it dangerous — its flexibility. When every field and mutation is a potential attack surface, one small oversight can cascade into massive compromise.
This was one of those cases where two small issues combined to form something far more severe. It’s a good reminder that in complex APIs, context is everything — and attackers only need one misconfiguration to find the full chain.