5 ms·
Hot take: this is a problem with GraphQL. GraphQL allows your front-end to query your data. Which is cool. But from the backend this is all really opaque (and
by marcus_holmes 2y ago
Hot take: this is a problem with GraphQL.
GraphQL allows your front-end to query your data. Which is cool. But from the backend this is all really opaque (and usually implemented by a 3rd party library that has no idea about your access control).
Unless you're going to implement your access control in the database itself (not the worst idea, certainly better than doing it in the front end), then it's very hard to unwrap the GraphQL query in backend code to work out exactly what records should be returned/restricted.
Implementing decent access control in the backend means understanding the query and implementing a whole set of models/classes/functions/whatever that grok the database schema and can make decisions about "if the user_id is XXX then it can/cannot see this image in this context" [0]. They obviously implemented this in the front end because that's a lot easier with GraphQL.
I'm not saying this is a good implementation of GraphQL and that therefore the problem lies with GraphQL exclusively. I'm saying that GraphQL makes this mistake easier to make because it explicitly tries to remove the need for the backend to understand the query and so makes this kind of complex security situation harder.
[0] e.g. a specific image may be publicly accessible from the user's profile, or only available to matches, or only in a chat context (but not group chats), and inaccessible at any time from blocked users, etc. You can easily come up with a bunch of complex edge cases for just this one case.
- strken 2y agoIt's pretty easy. Treat each resolver that retrieves data like it's a REST endpoint and secure it, and add a query allowlist that you append items to during your CI builds. You don't need to touch the AST or understand the context of the rest of the query. Just answer the question "can user ABC see the photos of user XYZ?" in the resolver that fetches the photos. If this is inefficient then prefetch some data or use a dataloader. Now, if you're using some magic library that turns GraphQL into SQL, that's going to be different.
- dangsux 2y ago[dead]
- mewpmewp2 2y agoStill I think this type of thing is much more likely to happen with GraphQL including various N + 1 and even worse performance issues. Like if you imagine having junior engs they will be much more likely to make the mistake with GraphQL than otherwise and it is harder to review as well. The permissions checking becomes a real spaghetti and difficult to understand in practice compared to just one by one checks.
- strken 2y agoThe permissions checking is one-by-one checks. It's exactly as hard a mistake to make in GraphQL as it is in REST unless you've got more resolvers than an equivalent REST app would have, which is unlikely and would mean GraphQL wasn't a good choice. I do think that you've got a good point about how the knowledge isn't widespread yet, that it's easier for frontend engineers to write awful expensive queries, and that GraphQL is very hard to secure against DoS unless you lock it down with query hashes.
- mewpmewp2 2y agoWouldn't it seem contra to the principles of GraphQL if you treat resolvers like rest endpoints? At this point, it's just RPC, no? It's not really a graph. Why didn't I just use RPC/Rest the whole time?
- strken 2y agoYou don't treat resolvers like RESTful endpoints. You check that the user has permission to access the object (edit: or other value) which the resolver returns. This has nothing to do with RPC and does not stop you using the "graph" part of GraphQL. For the purposes of comparing a REST API, where permissions checking is done for every endpoint, to a GraphQL API, where permissions checking is done for any resolver which loads data, it is necessary to compare the number of permissions checks you would need across the two services. This does not mean resolvers are in any way equivalent to RESTful endpoints except for comparing how many times you'd need to write `ctx.can('read', photo);` across the two, and even then the numbers will almost certainly be different because the APIs will be different.
- pistoleer 2y agoHow do you guys bridge the abstraction gap/wall between resolvers to prevent N+1 queries? I have the suspicion that GraphQL is great for exposing a really generic API, useful when you have no idea what shape the front end will take (how often is that?). But it comes at a heavy price; genericity is always the opposite of specialization. And optimization can only occur during specialization. Having worked with it for a bit over a year now, it really feels like GraphQL is just a different protocol for writing the same old REST CRUD, while introducing a huge framework with lots of annoying magic and language level reflection that isn't amendable to extension or modification according to the needs of the developers. Is that all worth it, just to reduce the amount of HTTP requests? Is it that much of a sacrilege to add specialized REST HTTP endpoints to remedy that otherwise?
- mewpmewp2 2y agoAlso if it's really the problem with HTTP Requests, you could still technically abstract multiple REST API/RPC calls into a single HTTP Request.
- strken 2y agoPutting a dataloader in front of batch APIs usually works okay. You end up with round trips but they're 1+1 and inside the data centre. I've used AST traversal to compute joins a couple of fields ahead + custom resolvers that only load their data if it wasn't loaded by the parent, but I don't think that's necessary to get decent performance and I wouldn't do it again unless there was a real business need. I agree that genericity is often the opposite of specialisation. I disagree that it's a heavy price. REST is pretty general. To my mind specialist APIs are things like streaming video, file uploads, anything that relies on caching in an intermediate layer, etc. and these are all examples of where you'd follow the established standards and add some RESTful routes/services. I don't think it's sacrilege to upload files in a different way to how you load your user dashboard or your interface for editing project permissions.
- rahkiin 2y agoI did not have this issue when using HotChocolate. You can easily give authorization rules to entities or properties of entities which will automatically be handled. Also to mutations
- luxcem 2y agoAny third party GraphQL library worth its salt should implement some kind of ACL. It seems to be the case with the most popular ones [1] [2]. One simple idea is to implement authorization in the data models. GraphQL delegate ~get~ and ~list~ to ressource model that could implement authorization based on the context of the request. [1] https://www.apollographql.com/docs/apollo-server/security/authentication/#delegating-authorization-to-models https://www.apollographql.com/docs/apollo-server/security/au... [2] https://docs.graphene-python.org/projects/django/en/latest/authorization/#user-based-queryset-filtering https://docs.graphene-python.org/projects/django/en/latest/a...
- knallfrosch 2y agoGraphQL requires you to either define per-property access, or precompile queries and put them into a whitelist. Everything else leaks data. https://hasura.io/docs/2.0/security/allow-list/ https://hasura.io/docs/2.0/security/allow-list/