pixellint

Pixels · engineers · PMs

buyer@example.com in the query is a finding

Meta's browser pixel still sees em=buyer@example.com in the wild. Pixellint flags unhashed emails on that transport (vendor.meta.pii.unhashed_email). Other vendors will log the URL even if they never intended to collect an email. Referer forwards the leak to the next page.

Who sees it

The vendor, every proxy, the browser history, HAR files you paste into Slack, corporate SSL inspection, CDN logs, and the next page's Referer if policy allows. Access logs are forever enough. A 1x1 GET with ?email=buyer@example.com is a spreadsheet waiting to be requested in discovery. HTTPS does not hide the query from the endpoints that terminate TLS.

Fragments never reach the server, so they are not a hidey-hole for the collector, and they still sit in the address bar and in HAR files. Userinfo (https://user:pass@host) is deprecated and flagged by core (core.url.userinfo_deprecated). Do not put an email in userinfo either. There is no approved slot in the URL for raw PII.

Meta's unhashed email rule

On facebook.com/tr, Pixellint reports vendor.meta.pii.unhashed_email when a parameter looks like a raw email. Meta wants customer information normalized and SHA-256 hashed before it is sent. The browser JS tag can hash. The image pixel cannot. CAPI wants the hex digest in user_data.em, 64 hex characters, after lowercase and trim. A 13-digit phone in em is a different mistake; a raw email is this one.

The CAPI pack has the same finding on query parameters and on the JSON body. Graph API query strings that carry ?em=buyer@example.com are still a leak even if the body is hashed. pixellint validate url on the browser GET; pixellint validate json on the CAPI body. Two artifacts, same PII rule family.

Referer leak

If the pixel URL is the document URL (a badly trafficked open pixel as a landing page) or if a subsequent navigation sends Referer with the full URL, every third party on the next page receives the email. Referrer-Policy can cut this down. Removing the email from the URL cuts it off. Do both. A strict policy with PII still in the pixel query only protects the next hop, not the vendor hop.

HAR exports and GTM preview snapshots are how emails leave the company in a zip file. Redact before you share. A finding in CI on the fixture URL is cheaper than a finding in a ticket screenshot. Do not put production emails in fixtures. Use a clearly fake address that still looks like an email so the rule can fire in tests, or use a hashed fixture for the clean path.

What to send instead

SHA-256 of a normalized email, in a body field the vendor named. Browser image pixels cannot hash; that is a reason to use the JS tag or CAPI. Do not roll a different hash. Vendors match SHA-256 of a specific normalization (trim, lowercase for email), not bcrypt, not MD5, not SHA-1. A bcrypt string in em will not match and may still look like PII to a linter.

IP and user-agent stay unhashed. Meta, TikTok, and others document those as plaintext match keys. Hashing them is a different finding (hashed_plaintext_field on CAPI packs). Hash the email. Leave the IP. Do not put either in the query string of a 1x1 if you can POST.

IDs that look like PII

Order ids and session ids are usually fine. User ids that are emails are not. If your external_id is the email, you have not anonymized anything. If your ord cache buster is the email, you have published it to Floodlight logs. If your cid is an email, GA4 will store an email as client id. Use an opaque id, and send the hashed email in the field named for email.

The URL below is a finding: numeric id is present, ev is present, and em is a raw mailbox. Hash it, or drop it from the query and send it hashed on CAPI. Do not Base64 the email as a workaround; Meta will not match Base64, and the mailbox is still recoverable. SHA-256 hex after lowercase and trim is the contract.

https://www.facebook.com/tr?id=123456789012345&ev=Lead&em=buyer@example.com

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.