Search code examples
graphqlgraphql-jsgraphene-python

GraphQL relations leaking data, even with context.user resolver already set. How to prevent data exposure via relations?


How is everyone doing authentication across relations to prevent data from being traversed via relations?

For example we have a Shop which has Users.

// Returns error as i've set custom resolver to allow only context.user.is_shop_owner
{
  shops {
    name
    users {
      email
      ...
    }
  }
}

This query is normally blocked with a custom resolver like context.user.is_shop_owner, so you cannot execute this from root query.

However, if a malicious person traverses relations to reach the users object he is able to get the sensitive user data.

// Data exposed un-intendedly due to relation traversal. How to prevent this?
{
  products {
    name
    price
    shop {
      users { ... } // boom, exposed
    }
  }
}

Is this a flaw in graphql? How are you guys working around this?

This is on a python-graphene stack btw.

Edit: Btw, i know we can do exclude_fields, but then i won't be able to access Users from the ShopNode, which is an important information to query for the ShopNode, so limiting fields are probably not a good idea. (edited)


Solution

  • I wound up setting custom resolvers for each node to block out the relations you want to limit access to based on the context.user. Refer to the code below in response to my question above.

    class ProductNode(DjangoObjectType):
        class Meta:
            model = Product
            interfaces = (graphene.relay.Node)
            # Exclude nodes where you do not need any access at all
            exclude_fields = ['users']
    
        # For nodes where you need specific/limited access, define custom resolvers.
        # This prevents the relation traversal loophole
        def resolve_shop(self, args, context, info):
            """ Allow access to nodes only for staff or shop owner and manager """
            if get_is_staff_user(context.user, self):
                return self.shop
    
            Operations.raise_forbidden_access_error()