Problem
permissions.ts calls shield(rules, { debug: true, allowExternalErrors: true }) — with no fallbackRule. graphql-shield's default fallbackRule is allow, so any GraphQL type or field that isn't given an explicit rule is readable by everyone, including unauthenticated callers.
This makes the permission layer fail open: the security posture of a new type/field depends on someone remembering to add a shield rule for it. Nothing fails loudly if they don't — the field is just silently public.
Why it matters (concrete instance)
This is exactly how the ModerationProfile.User deanonymization leak happened (fixed in #154): a relationship edge back to User had no shield rule, so it inherited the implicit allow, letting anyone map a pseudonymous moderation profile to the real account. Every future relationship edge to User / Email / ModerationProfile (or any other sensitive node) has the same trapdoor.
Direction (not a trivial flip)
The safe end state is fallbackRule: deny (fail closed), so a new field must be explicitly opened rather than explicitly closed. But flipping it is not a one-line change: a large amount of the schema currently relies on the implicit allow fallback — e.g. the User block's "*": allow, ModerationProfile's "*": allow, and all the types with no block at all (Issue, Discussion, Event, Comment, Channel, ServerConfig, …). Flipping to deny without first enumerating and explicitly allowing those public read paths would break most read queries.
Suggested work
Reference
Problem
permissions.tscallsshield(rules, { debug: true, allowExternalErrors: true })— with nofallbackRule. graphql-shield's defaultfallbackRuleisallow, so any GraphQL type or field that isn't given an explicit rule is readable by everyone, including unauthenticated callers.This makes the permission layer fail open: the security posture of a new type/field depends on someone remembering to add a shield rule for it. Nothing fails loudly if they don't — the field is just silently public.
Why it matters (concrete instance)
This is exactly how the
ModerationProfile.Userdeanonymization leak happened (fixed in #154): a relationship edge back toUserhad no shield rule, so it inherited the implicitallow, letting anyone map a pseudonymous moderation profile to the real account. Every future relationship edge toUser/Email/ModerationProfile(or any other sensitive node) has the same trapdoor.Direction (not a trivial flip)
The safe end state is
fallbackRule: deny(fail closed), so a new field must be explicitly opened rather than explicitly closed. But flipping it is not a one-line change: a large amount of the schema currently relies on the implicitallowfallback — e.g. theUserblock's"*": allow,ModerationProfile's"*": allow, and all the types with no block at all (Issue,Discussion,Event,Comment,Channel,ServerConfig, …). Flipping todenywithout first enumerating and explicitly allowing those public read paths would break most read queries.Suggested work
allow."*": allow(or field-level rules) to those types.fallbackRule: denyin theshield(...)options.deny).Reference
permissions.tsshield(...)options.