Graphql Introspection in Firestore
How Graphql Introspection Manifests in Firestore
GraphQL introspection in Firestore contexts typically emerges through misconfigured Apollo Server or similar GraphQL libraries deployed alongside Firestore databases. When developers enable introspection in production environments, attackers can map the entire GraphQL schema, revealing field names, types, resolvers, and potentially sensitive data structures that map directly to Firestore collections and documents.
The most common Firestore-specific manifestation occurs when GraphQL resolvers directly expose Firestore collections without proper access controls. For example, a resolver might query an entire collection:
const resolvers = {
Query: {
users: async () => {
const snapshot = await firestore.collection('users').get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
}
};
When introspection is enabled, an attacker can discover this users query and understand it returns all user documents. They can then use tools like GraphQL Playground or Insomnia to enumerate all fields, potentially exposing sensitive user data stored in Firestore.
Another Firestore-specific pattern involves nested queries that traverse document references. Consider a GraphQL schema where users have orders, and orders reference Firestore documents:
const resolvers = {
User: {
orders: async (parent) => {
const snapshot = await firestore.collection('orders')
.where('userId', '==', parent.id)
.get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
},
Order: {
items: async (parent) => {
const snapshot = await firestore.collection('orderItems')
.where('orderId', '==', parent.id)
.get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
}
};
With introspection enabled, attackers can discover these relationships and craft queries that traverse the entire data graph, potentially accessing data they shouldn't see through legitimate GraphQL queries that map to Firestore document relationships.
Firestore-Specific Detection
Detecting GraphQL introspection vulnerabilities in Firestore environments requires both automated scanning and manual verification. The middleBrick API security scanner specifically tests for this by attempting GraphQL introspection queries against detected endpoints. When middleBrick identifies a GraphQL endpoint, it sends the standard introspection query:
query IntrospectionQuery {
__schema {
types {
kind
name
fields {
name
args {
name
type {
name
kind
}
}
}
}
}
}
For Firestore-specific detection, middleBrick analyzes the schema structure to identify patterns that suggest Firestore integration. This includes detecting queries that follow Firestore collection naming conventions, resolvers that use Firestore SDK patterns, and field structures that map to Firestore document schemas.
Manual detection involves examining GraphQL endpoints for the presence of introspection queries. Using tools like curl or GraphQL clients, you can test:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query { __schema { types { name } } }"}' \
https://your-api.com/graphql
If this returns a complete schema, introspection is enabled. For Firestore-specific analysis, examine the returned types for patterns like:
- Collection names matching Firestore conventions (plural nouns)
- Fields that suggest document references (fields ending with
Id) - Query patterns that imply Firestore queries (pagination arguments like
first,offset)
middleBrick goes further by analyzing the actual runtime behavior. It attempts to execute queries that would be particularly revealing for Firestore-backed APIs, such as queries that might return entire collections or traverse document relationships that suggest Firestore's document reference system.
Firestore-Specific Remediation
Remediating GraphQL introspection vulnerabilities in Firestore environments requires a multi-layered approach. The first and most critical step is disabling introspection in production environments. With Apollo Server, this is straightforward:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV === 'development',
playground: process.env.NODE_ENV === 'development'
});
For Firestore-specific security, implement proper authorization at the resolver level using Firebase Admin SDK's security features. Instead of exposing entire collections, implement field-level security:
const resolvers = {
Query: {
user: async (_, { id }, context) => {
// Verify the requesting user has access to this document
if (!context.user || context.user.uid !== id) {
throw new Error('Unauthorized');
}
const doc = await firestore.collection('users').doc(id).get();
if (!doc.exists) throw new Error('Not found');
return { id: doc.id, ...doc.data() };
}
},
User: {
orders: async (parent, _, context) => {
// Only return orders belonging to the requesting user
const snapshot = await firestore.collection('orders')
.where('userId', '==', parent.id)
.where('ownerId', '==', context.user.uid)
.get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
}
};
Implement Firestore security rules as a second layer of defense. Even if GraphQL introspection reveals schema information, Firestore security rules prevent unauthorized data access:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /orders/{orderId} {
allow read, write: if request.auth.uid == resource.data.ownerId;
}
}
}
For production environments, consider implementing custom directives that automatically apply authorization checks based on the authenticated user's permissions. This creates a consistent security layer across all resolvers:
const typeDefs = gql`
directive @requireAuth on FIELD_DEFINITION
type Query {
user(id: ID!): User! @requireAuth
}
`;
const server = new ApolloServer({
typeDefs,
resolvers,
schemaTransforms: [
transformSchema(({ schema }) => {
const authDirective = schema.getDirective('requireAuth');
return mapSchema(schema, {
transformField: (fieldConfig, context) => {
if (fieldConfig.astNode?.directives?.some(d => d.name.value === 'requireAuth')) {
return {
...fieldConfig,
resolve: async (root, args, context, info) => {
if (!context.user) throw new Error('Unauthorized');
return fieldConfig.resolve(root, args, context, info);
}
};
}
return fieldConfig;
}
});
})
]
});
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |