The Problem Hiding in Plain Sight
Now I Get It! has a copyright takedown system -- someone submits a request, a Lambda processes it and publishes an SNS notification that gets emailed to our legal address. Standard stuff. But when I looked at what was actually in those emails, I found the full DynamoDB record dumped as raw JSON: claimant name, email, phone number, IP address, browser user agent, electronic signature. Every notification was a PII care package sitting in an inbox.
The same pattern existed in the admin action notifications. Approving, rejecting, or restoring a takedown request each fired their own SNS event through a shared _publish_event() helper -- which also dumped the full record. Even the Subject line of the initial notification included the claimant's name.
The Fix
The approach was a whitelist of non-PII fields: request ID, document URL, status, claimant role, work type, work description, and flagged status. Instead of json.dumps(record), each notification now gets formatted as readable plain text with an admin console link at the top so legal can click through for full details. The claimant's personal information stays in DynamoDB where it belongs -- accessible to authorized admins through the dashboard, not floating around in email.
One design consideration: work_description is user-supplied free text. A claimant could embed their own PII in it. Filtering free text for embedded personal data is a different class of problem, and the field is genuinely useful in the notification, so it stays.
The Lesson
The root cause was json.dumps() on an entire database record -- the "serialize everything" default. It's fast to write and easy to overlook in review because the code looks reasonable. The fix is to always build notification payloads from an explicit field list rather than serializing a data object. Default-closed beats default-open every time, especially when the data crosses a trust boundary like email.