Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions packages/hydrooj/src/handler/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,21 +438,30 @@ export const DomainApi = {
),
groups: Query(
Schema.object({
search: Schema.string(),
names: Schema.array(Schema.string()),
domainId: Schema.string().required(),
names: Schema.array(Schema.string()),
search: Schema.string(),
limit: Schema.number().step(1).max(100),
}),
async (ctx, args) => {
if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
const groups = await user.listGroup(args.domainId);
if (args.names?.length) {
return groups.filter((g) => args.names.includes(g.name));
return user.listGroup(args.domainId, undefined, args.names);
}
if (args.search) {
const searchLower = args.search.toLowerCase();
return groups.filter((g) => g.name.toLowerCase().includes(searchLower));
return user.searchGroups(args.domainId, args.search, args.limit ?? 10);
}
return groups;
return user.listGroup(args.domainId);
},
Comment on lines 448 to +450

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Authorize using the target domain context before returning groups.

Line 447 and Line 463 validate permissions on ctx.user, but the queried resource is args.domainId. This can expose another domain’s groups/user-groups when a caller passes a different domainId.

🔒 Suggested fix
 async (ctx, args) => {
-    if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
+    const targetUser = await user.getById(args.domainId, ctx.user._id);
+    if (!targetUser || (!targetUser.hasPerm(PERM.PERM_VIEW) && !targetUser.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN))) {
+        throw new PermissionError(PERM.PERM_VIEW);
+    }
     if (args.names?.length) {
         return user.listGroup(args.domainId, undefined, args.names);
     }
@@
 async (ctx, args) => {
-    if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
+    const targetUser = await user.getById(args.domainId, ctx.user._id);
+    if (!targetUser || (!targetUser.hasPerm(PERM.PERM_VIEW) && !targetUser.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN))) {
+        throw new PermissionError(PERM.PERM_VIEW);
+    }
     return user.listGroup(args.domainId, args.uid);
 },

Also applies to: 463-465

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrooj/src/handler/domain.ts` around lines 447 - 455, The
permission checks currently use ctx.user but the resource being queried is
args.domainId; update the code so authorization is performed against the target
domain's context before returning groups: obtain the domain-specific
user/authorization context for args.domainId (e.g., call a helper like
ctx.forDomain(args.domainId) / ctx.getDomainUser(args.domainId) or use an
existing domain authorization method), then call hasPerm/hasPriv on that
domain-scoped user instead of ctx.user, and only after that call user.listGroup,
user.searchGroups or user.listGroup with args.domainId; apply the same change to
the other similar block that calls user.listGroup/user.searchGroups to ensure
checks use the target domain context.

),
userGroups: Query(
Schema.object({
domainId: Schema.string().required(),
uid: Schema.number().step(1).required(),
}),
async (ctx, args) => {
if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
return user.listGroup(args.domainId, args.uid);
},
),
'domain.group': Mutation(
Expand Down
15 changes: 13 additions & 2 deletions packages/hydrooj/src/model/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,11 @@ class UserModel {
]);
}

static async listGroup(domainId: string, uid?: number) {
const groups = await collGroup.find(typeof uid === 'number' ? { domainId, uids: uid } : { domainId }).toArray();
static async listGroup(domainId: string, uid?: number, names?: string[]) {
const filter: Filter<GDoc> = { domainId };
if (typeof uid === 'number') filter.uids = uid;
if (names?.length) filter.name = { $in: names };
const groups = await collGroup.find(filter).toArray();
if (uid) {
groups.push({
_id: new ObjectId(), domainId, uids: [uid], name: uid.toString(),
Expand All @@ -489,6 +492,14 @@ class UserModel {
return groups;
}

static searchGroups(domainId: string, search: string, limit = 10) {
const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return collGroup.find({
domainId,
name: { $regex: escaped, $options: 'i' },
}).limit(limit).toArray();
}

static delGroup(domainId: string, name: string) {
deleteUserCache(domainId);
return collGroup.deleteOne({ domainId, name });
Expand Down
Loading