question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Consider allowing permissions to be set on a per-field basis

See original GitHub issue

Although 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:

  1. 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
  }
}
  1. 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

  2. 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 with User nodes

  3. 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:

  1. Forcibly set the info of the query to return a pre-determined result
  2. 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:closed
  • Created 5 years ago
  • Reactions:1
  • Comments:8

github_iconTop GitHub Comments

6reactions
maticzavcommented, Jun 9, 2018

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;

type User {
  name: String!
  age: Int!
  creditCard: CreditCard!
}

type CreditCard {
  number: String!
  securityCode: String!
}

and that our schema is similar to the following one;

type Query {
  user(id: ID!): User
}

Now, when you query the user, you first ask Prisma for a user with such and such id. All good! We also provide the info 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:

  1. We want to access Query.user field. GraphQL executes the resolver which returns a nested object obtained from Prisma.

  2. The result is forwarded to User type resolver, which has been auto-generated by graphql-yoga.

  3. 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 of User is forwarded to CreditCard resolver type.

  4. 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 apply rules to subfields of the nested types. In a case of the credit card, you could do something like this:

const permissions = shield({
  Query: {
    user: allow,
  },
  User: {
    name: allow,
    age: allow,
    creditCard: deny
   }
})

// or

const permissions = shield({
  Query: {
    user: allow,
  },
  CreditCard: {
    securityCode: deny
  }
})

Besides allow and deny, you could have used a more complex rule, such as isAdmin 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!

I would also like to stress, that in a well-structured project, graphql-shield should become entirely obsolete. All of its functionality can be achieved using schema manipulation. Nevertheless, the usual approach is far from ideal and rather hasty. That’s also the reason why I created graphql-shield - to ease the creation of permission layer.

5reactions
nolandgcommented, Aug 14, 2018

@maticzav Been reading a bunch of issues lately, thanks for your awesome clarity! you’re good at explaining graphql.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Consider allowing permissions to be set on a per-field basis #55
Consider allowing permissions to be set on a per-field basis #55 ... Developer sets a basic isAuthenticated permission on the query to ...
Read more >
Provide field-level security permissions - Jira - Atlassian
JRASERVER-29351 - Allow to specify field behaviour based on user ... I think that setting specific permissions to fields on a 'per usergroup'...
Read more >
Per Field Permission in Django REST Framework
I am using Django REST Framework to serialize a Django model. I have a ListCreateAPIView view to list the objects and a ...
Read more >
Manage hosted feature layer editing—ArcGIS Online Help
Click Save at the bottom of the Settings tab. Control edits on a per-field basis. If you enable attribute updates on a hosted...
Read more >
Establishing Windows File and Folder Level Permissions
When you set permissions, you are specifying what level of access students have to the folder and its files and what students can...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found