T05 · Unauthorized Access and Privilege Escalation
- Location
assets/templates/cloudfunctions/api/index.js:131- Finding
Private Group Membership Authorization Bypass
- Content
View full analysis
Vulnerability Details
File Location:
assets/templates/cloudfunctions/api/index.js, lines 131-137
Vulnerability Type: Missing authorization check
Risk Level: MediumVulnerable Code
js async function joinGroup(ctx, id) { const g = (await db.collection('groups').doc(id).get()).data; if (!g) return { error: '组不存在' }; const existing = (await db.collection('group_memberships').where({ openid: ctx.OPENID, groupId: id }).get()).data; if (existing.length) return { error: '已经在组里了' }; await db.collection('group_memberships').add({ data: { openid: ctx.OPENID, groupId: id, role: 'member' } }); return { ok: true }; }Technical Analysis
The
joinGrouphandler verifies that the requested group exists and that the caller is not already a member, but it does not verify whether the group is public or private. It also does not require an invitation, approval from the group owner, or any other server-side authorization evidence.The caller's identity is safely derived from the platform-provided
OPENID, so identity spoofing is not required. However, any authenticated user who learns or obtains a private group document ID can submit that ID to the join route and receive a valid membership record.The route exposing this operation is:
js if ((m = path.match(/^\/api\/groups\/([^/]+)\/join$/)) && method === 'POST') return await joinGroup(ctx, m[1]);Attack Path
- The attacker authenticates normally through the WeChat mini-program.
- The attacker obtains a private group ID through disclosure, client-side data, logs, shared links, or another application feature.
- The attacker invokes
POST /api/groups/{privateGroupId}/join. - The server retrieves the group but does not inspect its
typeor require an invitation. - The server inserts a
group_membershipsrecord with the attacker's platform-derivedOPENID. - Subsequent features that trust membership r ...[truncated 623 chars]
- Remediation
View remediation
Remediation Suggestions
- Permit direct membership creation only for public groups.
- For private groups, require a server-generated, cryptographically random, single-use invitation token or an owner-approved join request.
- Bind invitations to the intended group and, where appropriate, the intended user.
- Store invitation expiration and consumption state on the server and update it atomically with membership creation.
- Enforce authorization independently on every endpoint that reads or modifies group resources; do not rely solely on the join workflow.
- Create a unique database index on
(openid, groupId)to prevent duplicate memberships during concurrent requests. - Return a generic authorization error for private groups to avoid disclosing whether a supplied private group ID exists.
Example control flow:
js if (g.type === 'private') { const invitation = await validateInvitation(ctx.OPENID, id, ctx.body.inviteToken); if (!invitation) return { error: 'Not authorized to join this group' }; }
