# Profile Privacy & Visibility Controls

- **Pattern:** `ab-000757` (`community-social-engagement.profiles-identity.privacy-visibility-controls`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000757
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000757)

## Why it matters

Profiles without visibility controls violate GDPR Art. 25 (data protection by design) and CCPA §1798.100 (consumer right to control their data). Under OWASP A01 (Broken Access Control) and CWE-284, any user whose profile is marked private but still returned to strangers has had their consent overridden by a code defect. Beyond compliance, exposed private profiles leak follower graphs, contact hints, and behavioral patterns to scrapers and stalkers — a direct user safety failure.

## Severity rationale

High because bypassed privacy controls expose user data to unauthorized parties at query time, constituting a broken access control failure with immediate personal safety implications.

## Remediation

Gate every profile fetch through an access-control layer in `lib/db.ts` that checks the `is_private` flag and the viewer's follow status before returning any data. Never expose even partial profile fields to unauthorized viewers.

```ts
// lib/db.ts
export async function getUserProfile(userId: string, viewerId?: string) {
  const user = await db.user.findUnique({ where: { id: userId } })
  if (!user) return null
  if (!user.is_private) return user

  if (viewerId === userId) return user

  const isFollower = await db.follow.findFirst({
    where: { followingId: viewerId, followedId: userId }
  })
  return isFollower ? user : null
}
```

Also add `is_private Boolean @default(false)` to your Prisma `User` model if the field does not yet exist.

## Detection

- **ID:** `privacy-visibility-controls`
- **Severity:** `high`
- **What to look for:** Enumerate all relevant files and Check the user schema (Prisma, Drizzle, SQL) for privacy-related fields like `is_private`, `visibility`, `profile_public`, or similar. Check profile query logic for access control: RLS policies in PostgreSQL, API filters that limit profile data exposure, or conditional rendering in components. Verify that private profiles are not accessible to unauthenticated or non-follower users.
- **Pass criteria:** No more than 0 violations are acceptable. Profile visibility is controlled by a schema field (e.g., `is_private: boolean`). Queries fetching profiles for display apply access control filters. Private profiles are not displayed to users who haven't been granted access. Report the count of conforming instances found even on pass.
- **Fail criteria:** No visibility controls in the schema, or profiles are returned without permission checks, or private profiles are visible to all users. A partial or incomplete implementation does not count as pass.
- **Skip (N/A) when:** All user profiles are always public (no privacy feature implemented).
- **Cross-reference:** For accessibility evaluation of social interaction patterns, the Accessibility Basics audit covers focus management and screen reader support.
- **Detail on fail:** `"User schema has no visibility/privacy field. All profiles are public regardless of user preference."` or `"Profile API returns full profile data without checking privacy status or viewer relationship"`
- **Remediation:** Implement profile visibility controls:

  ```ts
  // Prisma schema
  model User {
    id String @id
    name String
    bio String?
    is_private Boolean @default false
    // ... other fields
  }

  // lib/db.ts - with access control
  export async function getUserProfile(userId: string, viewerId?: string) {
    const user = await db.user.findUnique({ where: { id: userId } })

    if (!user) return null
    if (!user.is_private) return user // Public profile

    // Private profile: only viewer or self can see
    if (viewerId === userId) return user

    // Check if viewer is a follower
    const isFollower = await db.follow.findFirst({
      where: { followingId: viewerId, followedId: userId }
    })

    if (isFollower) return user
    return null // Deny access
  }
  ```

## External references

- cwe CWE-284
- owasp A01
- gdpr Art. 25
- ccpa §1798.100

Taxons: access-control, privacy-consent

HTML version: https://auditbuffet.com/patterns/ab-000757
