# Users can withdraw consent from settings; withdrawal stops processing

- **Pattern:** `ab-000725` (`community-privacy-controls.data-rights.consent-withdrawal`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000725
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000725)

## Why it matters

GDPR Art. 7(3) is explicit: withdrawal of consent must be as easy as giving it, and processing must stop immediately upon withdrawal. A settings toggle that logs a withdrawal but continues sending events to analytics or ad networks is a phantom control — it creates the appearance of compliance while the violation continues. CCPA §1798.120 similarly requires that opt-out of data sale take effect within 15 business days. A backend that ignores the consent flag converts every post-withdrawal data collection event into a separate unlawful processing incident.

## Severity rationale

High because continued processing after documented withdrawal transforms a configuration issue into active, ongoing GDPR Art. 7(3) violations with each additional data collection event.

## Remediation

Check the most recent consent record in every data collection code path — do not cache the consent state in memory across requests. Add a middleware guard in `src/middleware.ts` or in the relevant route handler:

```ts
async function shouldCollectAnalytics(userId: string): Promise<boolean> {
  const latest = await db.userConsentAudit.findFirst({
    where: { userId, processingType: 'analytics' },
    orderBy: { consentedAt: 'desc' }
  });
  return latest?.consentGiven ?? false;
}

// In your analytics event handler
app.post('/api/events', async (req, res) => {
  if (!(await shouldCollectAnalytics(req.user.id))) {
    return res.status(204).end(); // silently discard
  }
  await trackEvent(req.body);
  res.status(200).end();
});
```

Flush any client-side analytics queue immediately on withdrawal — don't wait for the next page load.

## Detection

- **ID:** `consent-withdrawal`
- **Severity:** `high`
- **What to look for:** Enumerate every relevant item. Examine settings UI for consent withdrawal mechanisms. Check whether users can disable each non-essential processing type after opting in. Verify that withdrawal is immediately reflected — data collection stops right away. Check for withdrawal workflows and backend enforcement.
- **Pass criteria:** At least 1 of the following conditions is met. Settings page displays all consented processing types with toggles to withdraw. Withdrawal is immediate and causes data collection to stop. Logs show withdrawal with timestamp.
- **Fail criteria:** No withdrawal mechanism in settings. Users cannot disable analytics after enabling. Data collection continues even after withdrawal toggle.
- **Skip (N/A) when:** Never — withdrawal is a user right.
- **Detail on fail:** Describe the limitation. Example: `"Settings page shows analytics toggle but changes have no effect. Backend continues tracking regardless of toggle state."` or `"No UI to manage consent preferences after signup."`
- **Remediation:** Implement withdrawal with immediate enforcement:

  ```tsx
  // Settings component
  export function ConsentSettings({ userId }) {
    const [analyticsEnabled, setAnalyticsEnabled] = useState(false);

    const handleToggle = async (type: string, enabled: boolean) => {
      await fetch('/api/consent', {
        method: 'POST',
        body: JSON.stringify({ processingType: type, consentGiven: enabled })
      });

      // Immediately stop collection
      if (!enabled && type === 'analytics') {
        window.analyticsQueue?.flush();
        window.analyticsQueue = null;
      }
    };

    return (
      <label>
        <input
          type="checkbox"
          checked={analyticsEnabled}
          onChange={(e) => {
            setAnalyticsEnabled(e.target.checked);
            handleToggle('analytics', e.target.checked);
          }}
        />
        Allow anonymous usage analytics
      </label>
    );
  }
  ```

  Enforce withdrawal in backend:

  ```ts
  async function shouldCollectAnalytics(userId: string) {
    const consent = await db.userConsentAudit.findFirst({
      where: { userId, processingType: 'analytics' },
      orderBy: { consentedAt: 'desc' }
    });

    return consent?.consentGiven ?? false;
  }

  // Middleware
  app.post('/api/events', async (req, res) => {
    const userId = req.user.id;
    const shouldTrack = await shouldCollectAnalytics(userId);

    if (!shouldTrack) {
      return res.status(200).json({ accepted: false });
    }

    // Process event
  });
  ```

## External references

- gdpr Art. 7(3)
- ccpa §1798.120
- eprivacy Art. 5(3)

Taxons: privacy-consent

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