# Presence state computed from server connection state, not forgeable client heartbeats

- **Pattern:** `ab-000745` (`community-realtime.presence.server-computed-presence`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000745
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000745)

## Why it matters

Presence derived from client-controlled heartbeats can be forged: a disconnected or banned user can keep pinging to appear online, and a connected user can simply stop sending heartbeats to appear offline. CWE-287 (Improper Authentication) and OWASP A07 identify any system that trusts client-reported state for security decisions as broken. In a community platform, forged presence undermines moderation dashboards, violates user expectations about who can see them, and breaks features like "last seen" timestamps that users rely on to make trust decisions.

## Severity rationale

Critical because client-controlled presence signals can be forged indefinitely, allowing banned users to appear online and violating OWASP A07 authentication integrity.

## Remediation

Derive presence exclusively from server-observed connection events — `connection` and `disconnect`. Never update presence in response to a client-emitted event.

```ts
const presence = new Map<string, 'online' | 'offline'>();

io.on('connection', (socket) => {
  presence.set(socket.userId, 'online');
  io.emit('presence_update', { userId: socket.userId, status: 'online' });

  socket.on('disconnect', () => {
    presence.set(socket.userId, 'offline');
    io.emit('presence_update', { userId: socket.userId, status: 'offline', lastSeen: Date.now() });
  });

  // Never handle a 'set_status' event from the client
});
```

If users need a voluntary "Do Not Disturb" status, store it as a separate preference field — it must not override the authoritative online/offline derived from connection state.

## Detection

- **ID:** `server-computed-presence`
- **Severity:** `critical`
- **What to look for:** Enumerate all presence tracking mechanisms. For each, classify whether presence is derived from server-side connection state or from client-side heartbeats. Count the mechanisms that are server-authoritative.
- **Pass criteria:** Presence is derived from server-side connection state. A user is marked online only if they have at least 1 active, authenticated WebSocket/SSE connection. 100% of presence updates must be server-computed. Report even on pass: "Presence computed server-side via [mechanism]."
- **Fail criteria:** Presence is based on client-generated heartbeats or messages that the client controls.
- **Do NOT pass when:** Server tracks connection state but allows clients to override it via a custom "set_status" event — client-overridable presence is NOT a pass.
- **Skip (N/A) when:** Never — server-computed presence is critical for correctness.
- **Detail on fail:** `"Presence tracked via client heartbeat. A client could send heartbeats indefinitely to appear online after disconnection."`
- **Remediation:** Compute presence from server connection state:

  ```ts
  const userPresence = new Map<string, { userId: string; status: 'online' | 'offline'; lastSeen: number }>();

  io.on('connection', (socket) => {
    const userId = socket.userId;
    userPresence.set(userId, { userId, status: 'online', lastSeen: Date.now() });

    io.emit('presence_update', { userId, status: 'online' });

    socket.on('disconnect', () => {
      userPresence.set(userId, { userId, status: 'offline', lastSeen: Date.now() });
      io.emit('presence_update', { userId, status: 'offline' });
    });
  });

  // Never update presence based on client-provided heartbeats
  ```

## External references

- cwe CWE-287
- owasp A07

Taxons: access-control

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