From 41ac383a2766e851ce3e134a0dc486d6fc141bf1 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sun, 22 Feb 2026 08:46:19 -0800 Subject: fix(graphql): Enforce ownership checks for shadow-hide mutations --- src/graphql/user/resolvers.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/graphql/user/resolvers.ts b/src/graphql/user/resolvers.ts index 285c98f0..9773d8b0 100644 --- a/src/graphql/user/resolvers.ts +++ b/src/graphql/user/resolvers.ts @@ -98,6 +98,26 @@ const authenticatedPreferencesOperation = async ( }; }; +const ensureOwnerOrPrivileged = ( + identity: UserIdentity, + authorised: boolean, + targetUserId: number +) => { + if (!authorised && identity.id !== targetUserId) throw new Error('Unauthorized'); +}; + +const ensureBadgeOwnerOrPrivileged = async ( + identity: UserIdentity, + authorised: boolean, + badgeId: number +) => { + if (authorised) return; + + const ownsBadge = (await getUserBadges(identity.id)).some((badge) => badge.id === badgeId); + + if (!ownsBadge) throw new Error('Unauthorized'); +}; + export const resolvers: WithIndex = { Query: { User: async (_, args) => { @@ -123,15 +143,16 @@ export const resolvers: WithIndex = { }, Mutation: { shadowHideBadges: async (_, args, context) => - await authenticatedBadgesOperation( - context, - async (_, authorised) => await setShadowHidden(args.userId, authorised) - ), + await authenticatedBadgesOperation(context, async (identity, authorised) => { + ensureOwnerOrPrivileged(identity, authorised, args.userId); + + await setShadowHidden(args.userId, authorised); + }), shadowHideBadge: async (_, args, context) => - await authenticatedBadgesOperation( - context, - async () => await setShadowHiddenBadge(args.id, args.state == null ? true : args.state) - ), + await authenticatedBadgesOperation(context, async (identity, authorised) => { + await ensureBadgeOwnerOrPrivileged(identity, authorised, args.id); + await setShadowHiddenBadge(args.id, args.state == null ? true : args.state); + }), hideBadge: async (_, args, context) => await authenticatedBadgesOperation(context, async (identity) => { const allBadges = await getUserBadges(identity.id); -- cgit v1.2.3