GDPR Art. 17 and CCPA §1798.105 grant users the right to erasure — setting an is_deleted flag satisfies neither. Name, email, profile picture, and bio left in the database after a deletion request remain queryable by anyone with direct database access, exploitable via SQL injection (CWE-212), and constitute a reportable breach under GDPR Art. 83. Regulators have levied fines for exactly this pattern: data nominally 'deleted' but fully intact in the store.
Critical because retained PII after deletion directly violates GDPR Art. 17 and exposes the platform to regulatory fines and breach liability if that data is subsequently leaked.
Implement erasure at the database layer — either cascade-delete PII columns or replace them with an anonymized placeholder on the user row. Update all derived tables (social graph, activity logs) in the same transaction to prevent orphaned references. Example anonymization trigger:
CREATE FUNCTION anonymize_user_on_delete() RETURNS TRIGGER AS $$
BEGIN
UPDATE users SET
email = NULL,
phone = NULL,
profile_picture_url = NULL,
bio = NULL,
first_name = 'Deleted',
last_name = 'User ' || OLD.id,
deleted_at = NOW()
WHERE id = OLD.id;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_deletion_trigger
BEFORE DELETE ON users
FOR EACH ROW EXECUTE FUNCTION anonymize_user_on_delete();
Document the deletion window (e.g., immediate vs. 30-day grace) in your privacy policy to satisfy the "documented window" requirement.
ID: community-privacy-controls.visibility.erasure-completeness
Severity: critical
What to look for: Enumerate every relevant item. Examine deletion workflows. When a user requests account deletion, check whether all personally identifiable information (name, email, profile picture, bio, activity metadata) is completely removed or replaced with anonymized placeholders. Look for database deletion jobs, triggers, or API endpoints. Verify the deletion window is documented (immediately, 30 days, etc.).
Pass criteria: At least 1 of the following conditions is met. Account deletion triggers comprehensive removal or anonymization of all PII. Deleted accounts are either completely removed from the database or replaced with anonymized user data (e.g., username → "Deleted User 12345", email removed, profile data cleared). Deletion happens within a documented timeframe (typically immediately or within 24-48 hours). Before evaluating, extract and quote the relevant configuration or code patterns found. Report the count of items checked even on pass.
Fail criteria: Deletion only marks an is_deleted flag on the user record without removing personal data fields. PII remains in the database indefinitely after deletion request. Name, email, or other identifying data still visible in API responses or logs after deletion.
Do NOT pass when: The item exists only as a placeholder, stub, or TODO comment — partial implementation does not count as passing.
Skip (N/A) when: Never — right-to-erasure applies to all community platforms.
Cross-reference: For related security patterns, the Security Headers audit covers server-side hardening.
Detail on fail: Specify what PII remains after deletion. Example: "Account deletion only sets is_deleted=true. Email, phone number, profile picture remain in the database and are still queryable." or "Deleted user name remains visible on all their past posts — no anonymization applied."
Remediation: Implement comprehensive deletion that removes or anonymizes all PII:
-- Example: PostgreSQL trigger for user deletion
CREATE FUNCTION anonymize_user_on_delete() RETURNS TRIGGER AS $$
BEGIN
UPDATE users SET
email = NULL,
phone = NULL,
profile_picture_url = NULL,
bio = NULL,
first_name = 'Deleted',
last_name = 'User ' || NEW.id,
deleted_at = NOW()
WHERE id = NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_deletion_trigger
BEFORE DELETE ON users
FOR EACH ROW EXECUTE FUNCTION anonymize_user_on_delete();
Or in Node.js/TypeScript:
async function deleteUserPermanently(userId: string) {
// Anonymize user record
await db.user.update({
where: { id: userId },
data: {
email: null,
phone: null,
profilePictureUrl: null,
bio: null,
firstName: 'Deleted',
lastName: `User ${userId.slice(0, 8)}`,
}
});
// Remove from other sensitive tables
await db.socialGraph.deleteMany({ where: { OR: [{ followerId: userId }, { followingId: userId }] } });
}