Repository navigation
wiki: stop storing flags.ip_hash; scrub legacy unsalted values - #46
Conversation
…lues flags.ip_hash was a bare sha256 of CF-Connecting-IP, which enumeration reverses for any IPv4 address. The Worker now stores "hmac-sha256:<hex>" keyed by a new IP_HASH_SECRET Worker secret, and NULL when the secret is unset (no unsalted fallback). Nothing reads ip_hash: the flag rate limit keys FLAG_LIMITER on the raw IP (not persisted) and the per-target cap counts open flags, not IPs. Migration 0013 NULLs the legacy bare-hex values and keeps prefixed HMAC rows, so it is safe to apply any time after the deploy.
…eat, empty secret - Migration 0013 matches the HMAC prefix exactly (substr, not LIKE), says it is idempotent and must be re-run after a rollback to an older Worker, and that D1 Time Travel and export backups keep legacy values until they expire. - schema.ts no longer says ip_hash is never exported: research exports omit it, full D1 backups include it. - Test the empty-secret case and migration idempotency.
|
Review status: merged in Update 2026-10-14 (re-checked at
Original review (2026-10-14)Independent adversarial review of Verdict: the code is correct, but nothing reads Required before merge
Decisions for the maintainer
Merge order: release-affecting; merge with the other Fixes are in progress; this comment will be updated. Posted by the WikiLean PR swarm (lead); updated 2026-10-14. |
Status review (2026-10-14): ✅ Ready, with nitsThe code holds up. There is no keyless fallback, an empty secret counts as unset, and the 0013 scrub is idempotent and case-sensitive on the Nits
To ship (nothing deploys on merge)
Order: independent. Merge before #63 so the migrations land in numeric order. 🤖 Automated status pass by Claude Code over all 18 open PRs (#46–#63), posted for the repo owner. An independent verifier re-checked each finding. All 18 heads were also merged and tested together in a scratch tree; the only test failure that comes from combining them is #47 × #55 (three other pairs had mechanical text conflicts). Reviewed head: |
Per review: nothing reads flags.ip_hash, so an HMAC handle keeps data the Worker never uses, and IP_HASH_SECRET's `wrangler secret put` deploys a Worker outside the promoter. The flag endpoint now stores NULL; FLAG_LIMITER and the per-target open-flag cap stay the abuse defenses. - index.ts: drop flagIpHash, IP_HASH_PREFIX and the hexOf refactor; keep main's sha256Hex (the research export uses it) - env.ts, wiki/README.md: drop the IP_HASH_SECRET secret - 0013: unconditional `UPDATE flags SET ip_hash = NULL WHERE ip_hash IS NOT NULL`; safe any time since no Worker version reads the column; rerun after a rollback to an older Worker or a pre-scrub restore - schema.ts, docs/research-plan.md: describe the column as no longer written - tests: anonymous flag stores NULL; migration test covers every shape and a rerun Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until then the old Worker keeps writing the unsalted hash, so a scrub run earlier has to be repeated after the promotion, as after a rollback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes pushed (2026-10-14)These commits address the two earlier reviews on this PR: all three required items from the PR swarm's review, plus the What changed
Verification
Not changed / still open
Merge notes
🤖 Posted by Claude Code for the repo owner. An independent verifier checked the whole PR before |
wiki: stop storing flags.ip_hash; scrub legacy unsalted values
Problem
POST /api/flag/:slug(anonymous reader reports) storesflags.ip_hashas an unsalted SHA-256 ofCF-Connecting-IP(wiki/src/index.ts:2450at 37b7036:ipHash: ip ? await sha256Hex(ip) : null). There are only 2^32 IPv4 addresses, so anyone holding a row can recover the address by enumeration. For IPv4 the column is effectively plaintext.The value leaves D1 in full database exports (
wrangler d1 export). The research export (/api/research/export.jsonl) does not include the flags table.Nothing reads the column:
FLAG_LIMITERon the raw IP inside the ratelimit binding, which is not persisted./flags,/api/flags,/statsand the research export never select it.git grep -i ip_hashfinds only the writer, the schema and docs.Change
ip_hash = NULL. No IP-derived value is kept.FLAG_LIMITERand the per-target open-flag cap remain the abuse defenses; there have been 2 flags ever.0013_flags_ip_hash_scrub.sqlis an unconditionalUPDATE flags SET ip_hash = NULL WHERE ip_hash IS NOT NULL. It is idempotent and safe to run at any time, because no Worker version reads the column.schema.tsflags comment anddocs/research-plan.mdnow describe the column as no longer written, and the comment abovesha256Hex(still used by the research export) no longer calls it the flag handle.The first revision of this PR stored an HMAC under a new
IP_HASH_SECRETsecret. Review pointed out that nothing reads the handle, and thatwrangler secret putdeploys a Worker outside the Brain promoter, which can abort a frozen promotion. So this revision drops the handle and the secret (0acb967).How tested
npx tsc --noEmitpasses.npx vitest run: 917 pass, including:CF-Connecting-IPset storesip_hash: null;sha256Hexis main's implementation.Risks
wrangler d1 exportdumps keep the legacy values until they expire or are deleted.0013after one.Release-affecting (
wiki/src,wiki/migrations): ships only through the Brain release and promotion path. No new secret.What Jack must do
After the new Worker is live (running it earlier is harmless, but then repeat it), apply the scrub. Remote migration tracking is out of sync, as noted in 0012, so execute the file directly:
cd wiki && npx wrangler d1 execute wikilean --remote --file=migrations/0013_flags_ip_hash_scrub.sqlRe-run it after any rollback to an older Worker version, or a D1 restore to a point before the scrub. If this ships in the same release as #63, apply
0014before the deploy and0013after it.