Hashing an address doesn’t anonymise it
1 Aug 2026 · 6 min read
Sooner or later someone on your team suggests it: we log the addresses people type, that feels sensitive, so let’s hash them before they hit the log. It sounds like the responsible move. For Australian addresses it is close to useless, and the reason is worth understanding before you rely on it.
Hashing protects secrets. An address is not a secret.
A hash is hard to reverse only when the input is unpredictable. Passwords qualify: there are effectively infinite possibilities. Addresses do not. Every valid physical address in Australia is published in G-NAF — around 15 million of them — as an openly licensed dataset that anyone can download.
So an attacker holding your logs doesn’t need to break SHA-256. They hash the whole country and compare:
# the entire attack, in pseudocode
for address in gnaf_download: # ~15.4M rows, a free download
table[sha256(normalise(address))] = address
for row in leaked_logs:
print(table[row.hashed_query]) # instant lookup, no cracking requiredOn a laptop, building that table is minutes of work. The same logic applies to phone numbers, postcodes, dates of birth, and any other field drawn from a small, published set. Unsalted hashing of an address is obfuscation, not protection — and if it is written into a privacy policy as "anonymised", it is a claim that will not survive scrutiny.
What actually raises the cost: a key the database doesn’t contain
The fix is a keyed hash — an HMAC — where the key lives somewhere the data doesn’t. Now the precomputation attack requires the key, and a dump of the database alone does not contain it:
hash = HMAC-SHA256(key = pepper + tenant_salt, message = normalise(query))
- The pepper is a server-side secret, held in the application environment and never stored in the database — so a stolen database backup is not enough to reverse anything.
- A per-tenant salt keeps hash spaces separate. Without it, the same address produces the same hash for every customer, and two datasets can be joined on it.
- Normalise before hashing — lowercase, collapse whitespace — or the same address typed two ways produces two different hashes and your log becomes useless for the debugging you kept it for.
Call it pseudonymisation, because that is what it is
This is the part people get wrong in their privacy policies. If you hold the key, you can re-identify the data. Under the Privacy Act, information that can be re-identified is still personal information — so a keyed hash is pseudonymisation, not anonymisation. It genuinely reduces the blast radius of a leak. It does not take the data outside your obligations, and describing it as "anonymous" overstates it.
We hold the pepper for hashed WattleAddr search logs, so we say pseudonymisation in our own DPA and here. It is a smaller claim, and it is one we can stand behind.
Hashing one field is not enough
A hashed query sitting beside the matched address in plain text protects nothing — the answer is in the next column. Whatever you hash, check what else in the row identifies the same person: the matched result, a referrer URL with an address in the query string, a nginx access log carrying the full path. That last one catches people out repeatedly; the value never reaches the database at all, it is in a log file nobody thought about.
The thing that beats hashing: not storing it
A hash you keep forever is still a record. Retention does more for real-world risk than any transform: if the query text is gone after 30 days, the 30-day-old leak has nothing to reverse. Best of all is not writing it down — plenty of address logging exists to answer questions ("why did this lookup fail?") that a count, a timestamp and a status code answer just as well.
How WattleAddr handles it
Every plan, including Free, can set search-log privacy per workspace and per API key, in three modes:
- full — query and matched address stored as typed. The default, and the most useful for support.
- hashed — the query is stored as a keyed HMAC (server-side pepper plus a per-workspace salt) and the matched address text is not kept at all, because keeping it would defeat the hash.
- none — no query text and no matched address, only counts, timings and what billing needs.
Any of the three can also be applied to logs already stored, so switching mode scrubs your history rather than only changing what happens next. Retention is separately configurable, and the two work together: shorten the window, then decide what the window is allowed to hold.
None of this is exotic. It is the same reasoning you should apply to your own logs — the address your form collects is in your database long before it is in ours.