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.
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.
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:
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.
ID: community-privacy-controls.data-rights.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:
// 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:
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
});