Consider allowing permissions to be set on a per-field basis
See original GitHub issueAlthough I’m an avid user of GraphQL Shield - thank you for creating such a simple means of implementing permissions! - something that has always irked me is that when you implement permissions on a by-type basis, you effectively lose the relational power of GraphQL. Perhaps I’m wrong on this, but this is the issue I see, illustrated with an example:
- Developer of a social & eCommerce website implements a query to allow fetching user details for a user profile page , something like this:
{
user (input: UserWhereUniqueInput) {
name
age
}
}
-
Developer sets a basic
isAuthenticated
permission on the query to ensure that only logged in users can run this query, but adds no permissions beyond that -
A malicious user pokes around the publicly available schema, and finds out that the website stores payment information as
CreditCard
nodes that have one-to-one relations withUser
nodes -
Malicious user simply edits the query to the following, and can now steal people’s credit cards:
{
user (input: UserWhereUniqueInput) {
name
age
creditCard {
number
securityCode
}
}
}
I see two solutions to this:
- Forcibly set the
info
of the query to return a pre-determined result - Create a permissions system that parses
info
, compares it to one of several templates based on the user’s permissions level, and either throws an error or lets the query through
Since the first solution effectively turns GraphQL into a regular REST endpoint, thereby defeating the purpose, I ended up rolling my own solution that implements #2 using graphql-fields
. It even allows for functions to be passed in that can evaluate the input at runtime, and determine whether to authorize the operation or not based on the actual query input. Here’s an example with a mutation:
export default {
Mutation: {
createCollectionAsPartner: rule()(
async (parent, { collectionCreateInput }, ctx: Context, info) => {
const permissionFunction: PermissionFunction = (): boolean => {
return collectionCreateInput.collectionVersions.create.deleted !== false;
};
const permissionDefinition: PermissionDefinition = {
collectionVersions: {
create: {
name: true,
deleted: permissionFunction,
externalFiles: {
create: {
applicationType: true,
},
},
},
},
};
const verified = verifyMutationPermissions(
collectionCreateInput,
permissionDefinition,
'createCollectionAsPartner'
);
return verified;
}
),
},
};
Using this definition, this mutation would work, because name
is defined as true
in the PermissionDefinition
object:
mutation {
createCollectionAsPartner(collectionCreateInput: {collectionVersions: {create: { name: "12345"}}})
}
While this one would not, because approvedManually
is not defined in the PermissionDefinition
object:
mutation {
createCollectionAsPartner(collectionCreateInput: {collectionVersions: {create: { approvedManually:true}}})
}
This example is based on typings generated by Prisma, and limits what kind of input can be passed in when creating this type to the ones defined and equal to true
in the PermissionDefinition object. If a field is not defined in the object, or is defined but as either false
or a sync/async function that resolves to false
at runtime, the operation is denied. The end result is extremely granular control over what is and isn’t allowed, without sacrificing the ability to query/mutate by relations, the greatest strength in GraphQL.
Although I wouldn’t necessarily expect this to make it into the native library, I’d love to hear everyone’s thoughts on this approach! I was surprised to see how little attention this critical security issue is getting within the GraphQL community, but it’s possible that I’m just doing something totally wrong and this isn’t a problem with a correct setup.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:1
- Comments:8
Top GitHub Comments
Hey @artemzakharov 👋, I finally have some time to reply to your question! Here’s the idea;
GraphQL works recursively. I suppose you are using Prisma as a database which often abstracts the query process to the point where it seems like responses are not processed on your server thoroughly. As you might have guessed from this, they still are - all of them!
If you have some spare time, I highly recommend reading this article because it very well explains the idea behind GraphQL execution.
Back to the point. Let’s assume that your data model looks similar to this;
and that our schema is similar to the following one;
Now, when you query the user, you first ask Prisma for a user with such and such
id
. All good! We also provide theinfo
object with all the fields we want to access, and Prisma recursively obtains them for us. Up to this point, it seems like we have no direct control over the return values. The following part is the crucial one.GraphQL (GraphQL Yoga + GraphQL Tools, in your case) define resolvers for every single field in our schema, even if we haven’t explicitly told it to do so. Therefore, Query execution looks like this:
We want to access
Query.user
field. GraphQL executes the resolver which returns a nested object obtained from Prisma.The result is forwarded to
User
type resolver, which has been auto-generated bygraphql-yoga
.Each field that we have forwarded is also resolved by resolvers in
User
type and forwarded on until we reach final - scalar level. NOTE:creditCard
portion ofUser
is forwarded toCreditCard
resolver type.All of the values are composed together into a single response.
Finally, the solution! 🎉
You might have guessed where this was going before, otherwise, here’s the idea. Instead of wrapping and modifying
info
object, we just applyrules
to subfields of the nested types. In a case of the credit card, you could do something like this:Besides
allow
anddeny
, you could have used a more complex rule, such asisAdmin
or something similar for example, and only partially limit the access.I agree that
relations
are in a way still lost due to this approach. Nevertheless, such approach prevents many edge cases and reduces the number of tests needed for a genuinely secure codebase.I hope this solves your situation at least to a certain degree or sparks some ideas in your mind. In any case, let me know! 🙂
PS.: If you are using Prisma, I highly recommend you copy all the generated types to your schema and remove the fields that shouldn’t be exposed to the client. I would say this is one of the most overlooked security issues when using Prisma. Your server schema doesn’t need to match the generated one, not even on the type level. GraphQL should figure all of this by itself out of the box!
@maticzav Been reading a bunch of issues lately, thanks for your awesome clarity! you’re good at explaining graphql.