Profile visibility enforced only on the frontend is not enforced at all — any API client that bypasses the UI gets full profile data regardless of the user's privacy setting. OWASP A01:2021 (Broken Access Control) is the top web application vulnerability for exactly this reason. GDPR Art. 25 (privacy by design) requires access restriction to be a default, not an afterthought. A two-tier public/private model also fails users who want followers-only visibility, forcing them to choose between full exposure and full lockdown.
Medium because unauthorized profile data access requires a direct API call, but the exposure is complete — every field on the profile is returned — once the frontend enforcement is bypassed.
Add at least three visibility tiers to the users table and enforce the check in every API handler that returns profile data. Never rely on frontend filtering. In your profile API route (e.g., src/app/api/users/[id]/route.ts):
async function getUserProfile(requesterId: string, targetUserId: string) {
const user = await db.user.findUnique({ where: { id: targetUserId } });
const canView = await checkProfileVisibility(requesterId, targetUserId, user.profile_visibility);
if (!canView) throw new ForbiddenError('Profile not accessible');
return sanitizeProfile(user, requesterId);
}
Schema addition:
ALTER TABLE users ADD COLUMN profile_visibility VARCHAR(20)
DEFAULT 'public'
CHECK (profile_visibility IN ('public', 'friends_only', 'followers_only', 'private'));
ID: community-privacy-controls.visibility.visibility-tiers-enforced
Severity: medium
What to look for: Enumerate every relevant item. Check the database schema for privacy/visibility fields on user profiles. Look for at least three tiers (e.g., public, friends-only, private, or similar variants). Examine API endpoints that return user data to verify that visibility is checked at the API layer (not just frontend filtering). Search for authorization middleware or permission checks in route handlers.
Pass criteria: At least 1 of the following conditions is met. Profile visibility is defined in the database with at least three distinct tiers. API endpoints that return user profile data check visibility level before responding. Unauthorized visibility levels return 404 or limited data, not a 403 (to prevent user enumeration in some cases, though this is context-dependent).
Fail criteria: Only one or two visibility tiers exist (e.g., public or nothing). Visibility is enforced only on the frontend (backend would return full data if called directly). No authorization middleware on profile endpoints.
Skip (N/A) when: Never — visibility control is fundamental to community platforms.
Cross-reference: For user-facing accessibility and compliance, the Accessibility Basics audit covers foundational requirements.
Detail on fail: Describe the visibility limitation. Example: "Only 'public' and 'private' tiers exist; no granular control (friends-only, followers-only)." or "API endpoint /users/:id returns full profile data without checking visibility field — frontend filtering only."
Remediation: Implement multi-tier visibility with API-layer enforcement:
ALTER TABLE users ADD COLUMN profile_visibility VARCHAR(20)
CHECK (profile_visibility IN ('public', 'friends_only', 'followers_only', 'private'));
Enforce in API routes:
async function getUserProfile(requesterId: string, targetUserId: string) {
const user = await db.user.findUnique({ where: { id: targetUserId } });
const canView = await checkProfileVisibility(requesterId, targetUserId, user.profile_visibility);
if (!canView) {
throw new ForbiddenError('Profile not accessible');
}
return sanitizeProfile(user, requesterId);
}
async function checkProfileVisibility(viewerId: string, targetId: string, visibility: string) {
if (visibility === 'public') return true;
if (visibility === 'private') return viewerId === targetId;
if (visibility === 'friends_only') {
const isFriend = await db.friendship.findUnique({
where: { follower_following: { followerId: viewerId, followingId: targetId } }
});
return !!isFriend;
}
return false;
}