When a user marks their profile private, they are expressing a consent boundary — covered under GDPR Art. 25 and CCPA §1798.120. An instant-follow model that ignores the private flag grants content access without approval, defeating the user's explicit privacy setting. Under CWE-284, this is a failed authorization check: the platform makes the access control decision on behalf of the user rather than routing it through the user's own approval queue. The fix requires a FollowRequest table to hold pending state across the approval workflow.
Low because the gap only surfaces when private accounts exist and is not a data exfiltration vector, but it directly violates the user's stated privacy preference.
Add a FollowRequest model with a status enum to your Prisma schema, then branch your follow logic on the target account's is_private flag before deciding whether to create an instant follow or a pending request.
// lib/follow.ts
export async function requestFollow(followerId: string, followedId: string) {
const target = await db.user.findUniqueOrThrow({ where: { id: followedId } })
if (!target.is_private) {
return db.follow.create({ data: { followerId, followedId } })
}
return db.followRequest.create({
data: { followerId, followedId, status: 'pending' }
})
}
export async function approveFollowRequest(requestId: string, approverId: string) {
const req = await db.followRequest.findUniqueOrThrow({ where: { id: requestId } })
if (req.followedId !== approverId) throw new Error('Unauthorized')
await db.$transaction([
db.follow.create({ data: { followerId: req.followerId, followedId: req.followedId } }),
db.followRequest.update({ where: { id: requestId }, data: { status: 'accepted' } })
])
}
ID: community-social-engagement.follow-graph.friend-request-approval
Severity: low
What to look for: Enumerate all relevant files and If profile privacy controls exist (is_private field), check for a friend request flow. Look for a FollowRequest or ConnectionRequest table with a status field (pending, accepted, rejected). Check for UI elements allowing users to "Accept" or "Decline" follow requests. Verify that content is blocked until the follow request is approved for private account users.
Pass criteria: At least 1 conforming pattern must exist. If is_private is supported, follow requests use a pending status. Users can approve or decline. Content from private accounts is blocked until approval.
Fail criteria: Private accounts exist but follow requests are instant/no approval step, or pending requests are not displayed to the recipient.
Skip (N/A) when: No profile privacy feature, or all profiles are public.
Detail on fail: "is_private field exists but follow requests are instant — private account follow flow not enforced" or "No FollowRequest table; pending follows not tracked"
Remediation: Implement follow request workflow:
// Prisma schema
model FollowRequest {
id String @id @default(cuid())
followerId String
followedId String
status String @default("pending") // pending, accepted, rejected
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
follower User @relation(name: "sent_requests", fields: [followerId], references: [id], onDelete: Cascade)
followed User @relation(name: "received_requests", fields: [followedId], references: [id], onDelete: Cascade)
@@unique([followerId, followedId])
}
// Follow logic for private accounts
export async function followUser(followerId: string, followedId: string) {
const followedUser = await db.user.findUnique({ where: { id: followedId } })
if (followedUser.is_private) {
// Create pending request
return db.followRequest.create({
data: { followerId, followedId, status: 'pending' }
})
} else {
// Instant follow for public accounts
return db.follow.create({
data: { followerId, followedId }
})
}
}
// Accept follow request
export async function acceptFollowRequest(requestId: string) {
const req = await db.followRequest.findUnique({ where: { id: requestId } })
await db.follow.create({ data: { followerId: req.followerId, followedId: req.followedId } })
await db.followRequest.update({ where: { id: requestId }, data: { status: 'accepted' } })
}