Back to all writeups
API
API Security
7 min read•Published 2026-07-22

Dissecting Broken Object Level Authorization in Federated GraphQL Subgraphs

A deep dive into how decoupled microservice resolvers in Apollo Federation can fail to propagate tenant authorization contexts, leading to stealthy data cross-contamination.


Overview

GraphQL Federation enables organizations to split massive monolithic schemas into independently deployable subgraphs. While this enhances developer velocity, it decentralizes the authorization boundary. In this writeup, we analyze a real-world scenario where a sub-graph entity resolver relied on the gateway for tenant enforcement, leaving direct entity references unprotected.

The Attack Surface

In Apollo Federation, entity resolution uses the `_entities` query:

graphql
query ($representations: [_Any!]!) {
  _entities(representations: $representations) {
    ... on OrganizationInvoice {
      id
      amount
      billingContactEmail
      lineItems {
        description
        total
      }
    }
  }
}

When the frontend gateway processes a query, it resolves composite types across multiple microservices. If the `BillingService` subgraph does not explicitly inspect the requesting user's tenant ID from the injected JWT headers, any valid representation key can be hydrated.

Root Cause

The microservice resolver assumed authentication performed by the API Gateway was sufficient proof of authorization:

typescript
// Vulnerable Resolver in Billing Subgraph
const resolvers = {
  OrganizationInvoice: {
    __resolveReference(reference, context) {
      // Flaw: reference.id is queried directly without verifying context.user.tenantId
      return db.invoices.findById(reference.id);
    }
  }
};
Technical Impact

An attacker with an account in Tenant A could capture a legitimate GraphQL request, swap the `invoiceId` with an enumerated or intercepted UUID belonging to Tenant B, and the federated query engine would fetch Tenant B's sensitive financial data.

Remediation

1. **Enforce Tenant Context in Resolvers**: Never trust entity keys alone. Always join against the authenticated user's organization scope:

typescript
// Secure Resolver
const resolvers = {
  OrganizationInvoice: {
    async __resolveReference(reference, context) {
      const invoice = await db.invoices.findOne({
        where: {
          id: reference.id,
          tenantId: context.user.tenantId // Mandatory scope assertion
        }
      });
      if (!invoice) throw new ForbiddenError('Access Denied');
      return invoice;
    }
  }
};

2. **Schema Directives**: Apply authorization directives (e.g., `@auth(requires: TENANT_MATCH)`) evaluated during AST traversal before query execution.

#GraphQL
#BOLA
#Authorization
#Microservices
#Apollo