DEV Community

Cover image for Don’t Hash IPs Without Salt: The Math That Breaks It
techpotions
techpotions

Posted on • Originally published at techpotions.com

Don’t Hash IPs Without Salt: The Math That Breaks It

The popular trick to hash ip address analytics gdpr—taking a raw SHA-256 of a visitor’s IP—is a privacy illusion that collapses under trivial arithmetic. You are not anonymising the address. You are just storing it inside a hash, and anyone who holds your logs can reverse every single one of them with less computational effort than it takes to open a browser tab.

Why ‘hash ip address analytics gdpr’ demands a salted hash

Unsalted hashes are not pseudonymisation. They are a slower form of the original IP. The entire IPv4 address space contains about 4.3 billion possible addresses (232). That number is trivially small for a modern CPU. An attacker who obtains your unsalted hashes can:

  1. Enumerate every IPv4 address from 0.0.0.0 to 255.255.255.255.
  2. Hash each one with the same function you used.
  3. Build a complete reverse lookup table in minutes.

Once the table exists, any unsalted hash in your analytics database becomes a simple dict lookup back to the original IP. The hash is no harder to crack than the IP itself—it’s just an opaque wrapper. GDPR’s bar for anonymisation is high, and a reversible transformation on a keyspace this small fails it completely.

Attack step Unsalted hash (SHA-256) Salted hash (SHA-256 with secret)
Enumerate all 4.3B IPs offline ✅ Possible ❌ Impossible without salt
Precompute a universal reverse dictionary ✅ Trivial ❌ Useless without that specific salt
Reverse a production hash after a leak ~minutes on commodity hardware ~need to first steal the salt
Privacy guarantee None Pseudonymisation with a secrets boundary

Salt turns the arithmetic problem into a secrets-management problem

Once you salt, privacy is no longer about math alone—it’s about operational security. A salted hash is constructed as:

hash(secret_salt + separator + ip)
Enter fullscreen mode Exit fullscreen mode

With a cryptographically random, long-running secret, there is no way to precompute anything. The attacker’s only path is to steal the salt first, which changes the game from “anyone with the hashes can reverse them” to “only someone who also compromises the salt can reverse them.” That’s exactly the shift you want: you’ve given yourself a hard but manageable secret-keeping problem, instead of leaving a public arithmetic lock that everyone can pick.

Operationally, that means:

  • Keep the salt out of your repository — environment variable only.
  • Never log it.
  • Rotate the salt consciously, accepting that rotation breaks continuity with past stored hashes (a feature for privacy, a nuisance for long-term analytics).
  • Use a dev fallback so local development works, but enforce in production that the real salt is present. Our own fallback value is intentionally named tp-fallback-salt-rotate-me-in-prod to scream “replace me.”

If you’re building privacy-respecting analytics from scratch, we can help you get the operational details right— take a look at how we build web services with privacy baked into the stack.

Implementing a privacy-safe IP hash in TypeScript

The implementation is small and runs anywhere Web Crypto is available (browsers, Node, Deno, workers). Here’s the core logic, adapted from lib/hash.ts in our analytics pipeline:

async function hashIP(ip: string, salt: string): Promise<string> {
  // Never hash an empty or missing value—return a sentinel instead
  if (!ip) return 'unknown';

  // Construct the message: salt:ip
  const msg = new TextEncoder().encode(`${salt}:${ip}`);
  const hashBuffer = await crypto.subtle.digest('SHA-256', msg);
  // Convert to hex string
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
Enter fullscreen mode Exit fullscreen mode

Takeaways from this snippet:

  1. Always include the salt before the IP. Appending salt to ip or using a fixed prefix doesn’t change the security property, but this ordering aligns with the classic HMAC mental model and avoids accidental collisions if someone later adds prefixes to IPs.
  2. The separator (colon) is irrelevant to brute-force hardness—it’s a readability choice. The salt’s secrecy is doing all the work.
  3. Return a distinguishable value for missing input. Hashing an empty string would produce a deterministic output that could be mistaken for a real visitor. The 'unknown' sentinel keeps absent data separate and traceable.

When you start a new project, we set this up as part of your foundation so you never ship with a vanilla unsalted hash.

Operational rules that keep the salt a secret

The strongest hash is worthless if the salt lives in main.

Rule Why
Salt comes from process.env.SALT_SECRET, never a config file Environment variables are the least likely to leak via source code, and modern platforms make them easy to rotate.
No default to a production-valid value Our dev fallback literally contains the words “fallback” and “rotate-me”; production code can detect it and refuse to start.
Rotate on a schedule you’ve deliberately chosen If you rotate daily, you keep only 24 hours of linkable history. That’s a privacy control. If you rotate yearly, you prioritise long-term trend analysis. The choice is yours, but make it explicit.
Never log, trace, or include the salt in error messages Even a debug log can turn into a permanent leak in a log aggregator.
Store hashes, not the raw IP + salt Once hashed, discard the original IP. There should be no path from the hash back to the IP inside your application.

What this does—and does not—do for GDPR compliance

Salted hashing is pseudonymisation, not anonymisation, and it’s not legal advice.

Pseudonymisation means the data can no longer be attributed to a specific person without additional information (the salt, in this case). GDPR encourages pseudonymisation as a technical measure that reduces risk and can help satisfy the data-protection-by-default requirement. However, whether it is sufficient for your particular processing depends on:

  • What other data you associate with the hashed IP
  • How long you retain it
  • The likelihood and impact of re-identification if the salt were exposed
  • Your legal basis for processing

Our approach to privacy-first architecture treats every component like this, reducing risk at each layer. But only a data protection specialist reviewing your full system can tell you whether you meet GDPR’s standards.

FAQ

Does salting IP hashes make my analytics GDPR compliant?

No single technical measure guarantees GDPR compliance. Salted hashing is pseudonymisation—it reduces re-identification risk but doesn’t eliminate it altogether. Whether this measure is sufficient depends on the rest of your processing: data context, retention, intended purpose, and any other safeguards you apply. Always consult a data protection specialist.

What happens to my analytics when I rotate the salt?

Rotation intentionally breaks the mapping between your historical hashes and current data. Previous hashes become orphaned, which is a privacy feature—if an attacker obtains old hashes, they can’t link them to fresh data. However, it also disrupts long-term visitor counts and cohort analysis. Choose a rotation cadence after deciding whether you value continuity or privacy more for your use case.

Can I use one salt and never rotate it?

It is only safe as long as the salt remains secret. If the static salt is ever exposed—through a code leak, environment dump, or misconfigured access—every hash you ever produced becomes as reversible as an unsalted hash. Treat the salt like a cryptographic secret: store it in an environment variable, never in source, and consider a rotation policy that fits your threat model.

Top comments (0)