fix(pii): never echo original PII and generate guaranteed-invalid dummies#594
Conversation
…mies Part of dataiku#465 (the lookup-first mapping half already landed in dataiku#468). - GenerateReplacement now guarantees the replacement differs from the original, falling back to the [REDACTED_...] placeholder after repeated generator collisions, so a dummy can never leak the PII it is meant to mask. - StreetGenerator uses pickExcluding like the other list generators. - SSNs use the never-issued 900-999 area range, phone numbers the reserved 555-0100..0199 fictional block, and credit card numbers deliberately fail the Luhn checksum, so dummies look realistic but can never collide with a real person's data. - Tests for the new invariants; gofmt pass over the touched test file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
A note on the failing "Check PR scope" job: I think this is a false positive worth a closer look, on two levels. The changeset is misclassified as docs. In Even with that fixed, compliant PRs would still trip the check. CONTRIBUTING requires code changes to ship with tests, and user-visible changes to ship with a changeset. So a PR that follows the checklist always touches code + test + changeset, which counts as 3 areas (code, test, chore) and fails. Splitting along those lines would produce a code PR with no tests, which the same guidelines forbid. I would suggest the classifier treat |
hanneshapke
left a comment
There was a problem hiding this comment.
Thank you for the great PR.
How would you extend the list of names, city and locations?
| @@ -0,0 +1,5 @@ | |||
| --- | |||
| "kiji-privacy-proxy": patch | |||
There was a problem hiding this comment.
@rhiannalitchfield Do you mind removing the changeset file from your PR. It will be set by the maintainers when we cut the next version.
Thank you!
There was a problem hiding this comment.
Removed in 571d443. Good to know changesets are handled by the maintainers at release time, thank you!
|
|
||
| // Randomly choose format | ||
| formats := []string{"%d-%d-%d", "%d.%d.%d", "(%d) %d-%d"} | ||
| formats := []string{"%d-555-%04d", "%d.555.%04d", "(%d) 555-%04d"} |
| // drawn from 900-999, which the SSA has never issued and has reserved against | ||
| // future issuance, so the output looks real but can never collide with an | ||
| // actual person's SSN. | ||
| func SSNGenerator(rng *rand.Rand, original string) string { |
There was a problem hiding this comment.
The solution works. If you want to make it even more realistic, avoid all prefixes shown in this list
https://www.ssa.gov/employer/ssnvs/highgroup.txt
These are all active US SSNs
There was a problem hiding this comment.
Good pointer, and it sent me down a useful rabbit hole. I'd argue the 900-999 range already avoids every prefix in that list, with a stronger guarantee than consuming the list would give:
- The high group list was frozen on June 25, 2011, the day the SSA switched to randomized assignment. Since then the SSA assigns from essentially all of 001-899 (excluding 000 and 666), so a prefix being absent from the list no longer means it is inactive. Generating from the list's complement within 001-899 would still produce SSNs that can belong to real people.
- Every prefix that has ever been issued (and everything in the list) lies in 001-899. 900-999 is reserved against SSN issuance, so the current generator avoids 100% of the listed prefixes by construction, and stays correct as randomization fills in the rest of 001-899.
Your comment did surface one real gap though: IRS ITINs also start with 9 and use group digits 50-65, 70-88, 90-92, and 94-99, so a generated 9XX dummy could have matched a real ITIN. Fixed in 6d677d8 by constraining the group digits to 10-49, with a test asserting both ranges.
There was a problem hiding this comment.
Wow, your comment is incredible insightful! Thank you!
I didn't know that the SSA changed to random numbers. In that light, going with the 9XX prefix makes a lot of sense. Thank you also for considering ITINs!
Requested in review: changesets for this repo are set by the maintainers when cutting the next version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up: 9XX numbers with group digits 50-65, 70-88, 90-92, or 94-99 are the IRS ITIN space, so a generated dummy could match a real person's ITIN. Constrain the group to 10-49 so the output avoids both issued SSN areas (900-999 was never assigned) and all ITIN ranges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
My preference would be to move the pools out of the inline Go arrays into data files embedded with
Issue #465 already floats the embed.FS idea, so I'd scope that as a follow-up PR rather than growing this one, if that works for you. |
| // drawn from 900-999, which the SSA has never issued and has reserved against | ||
| // future issuance, so the output looks real but can never collide with an | ||
| // actual person's SSN. | ||
| func SSNGenerator(rng *rand.Rand, original string) string { |
There was a problem hiding this comment.
Wow, your comment is incredible insightful! Thank you!
I didn't know that the SSA changed to random numbers. In that light, going with the 9XX prefix makes a lot of sense. Thank you also for considering ITINs!
|
Thank you @rhiannalitchfield for all your updates - this PR is looking good! |
|
@rhiannalitchfield I really like the thought towards "It also opens the door to locale-aware pools later" |
Purpose
Part of #465. The lookup-first mapping reuse (step 1 of the issue) already landed in #468, so this PR covers the remaining generator-realism and test work.
Two gaps in replacement generation:
pickExcluding, butStreetGeneratordid not, and the format-based generators (email, phone, SSN, etc.) had no guard at all. A dummy equal to the original silently leaks the PII to the upstream provider.What Changed
GeneratorService.GenerateReplacementnow guarantees the replacement differs from the original: the label's generator gets a few attempts, then it falls back to the deterministic[REDACTED_{label}_{hash}]placeholder, which embeds a hash of the original and can never equal it.StreetGeneratorusespickExcludinglike the other list generators.Testing
GenerateReplacementnever returns it, across all 26 labels plus the generic fallback.StreetGeneratorexcludes the original.golangci-lintreports 0 issues. Also includes a gofmt pass over pre-existing formatting drift in the test file this PR touches.Changeset included (patch).
🤖 Generated with Claude Code