DEV Community

Bedirhan Elibol
Bedirhan Elibol

Posted on

Four privacy bugs that get through code review

Privacy bugs rarely break a test. The page renders, the form submits, the dashboard fills up with sessions. They show up later, in a complaint or an audit, and by then the code has been in production for months.

Here are four that show up often in pull requests. Each one is small to fix.

1. A cookie banner with no way to refuse

export function CookieBanner() {
  return (
    <div className="cookie-banner">
      <p>We use cookies to improve your experience.</p>
      <button onClick={acceptAll}>Accept all</button>
      <a href="/cookies">Settings</a>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

GDPR and the ePrivacy rules expect refusing to be as easy as accepting. Regulators in France have fined sites for a banner where Accept is one click and Reject needs a trip through a settings page.

The fix is a second button with the same weight:

<button onClick={acceptAll}>Accept all</button>
<button onClick={rejectAll}>Reject all</button>
Enter fullscreen mode Exit fullscreen mode

To find this in your own code, open the banner component and search for "reject", "decline" or "necessary only". If there's an Accept button and none of those words, users have no direct way to say no.

2. The consent cookie lives too long

Cookies.set('cookie_consent', 'all', { expires: 730 }); // 2 years
Enter fullscreen mode Exit fullscreen mode

The CNIL recommends keeping a consent choice for 13 months at most, then asking again. Watch the units, because each library uses a different one: js-cookie's expires is in days, Express res.cookie takes maxAge in milliseconds, and the Next.js cookies API takes maxAge in seconds. Copy maxAge: 31536000 from a Next.js file into Express and one year becomes about eight hours. The reverse mistake turns a year into centuries.

Cookies.set('cookie_consent', 'all', { expires: 365 });
Enter fullscreen mode Exit fullscreen mode

3. Session replay that records form input

posthog.init(KEY, {
  session_recording: { maskAllInputs: false },
});
Enter fullscreen mode Exit fullscreen mode

PostHog and Sentry Replay mask inputs by default. Someone turns masking off to debug a checkout form, the PR gets merged, and from then on the replays store addresses, phone numbers and whatever else people type. That data now sits with a third party, and your privacy policy probably doesn't list it for that purpose.

Leave masking on and unmask only the fields you really need, using the tool's per-element option.

4. Personal data in the URL

router.push(`/onboarding?email=${email}`);
Enter fullscreen mode Exit fullscreen mode

Query strings end up in server logs, analytics, browser history and the Referer header sent to any third-party script on the next page. An email in the URL gets copied to all of those places.

Pass it in state or in the request body, or look it up on the server from the session:

router.push('/onboarding');
Enter fullscreen mode Exit fullscreen mode

One more you have to test by hand

Loading analytics or ad scripts before the user has answered the banner. This one is hard to see in a diff because it depends on load order. Open the site in a private window, open the network tab, and check what loads before you click anything.

Catching these on the pull request

I build Zelsis, a check that runs on every pull request. The first four patterns above are flagged there as medium findings: they show up as warnings on the PR and don't fail the check. Writing plain emails or phone numbers to logs, or sending them unmasked to an error tracker, is reported as high.

Public repos scan free, and you can paste one on zelsis.com to see a result without an account.

What privacy bug have you found in your own code after it shipped?

Top comments (0)