Skip to content

fix(attribution): ignore shared/NAT IPs in fingerprint matching + CDN client-IP header#26

Merged
onamfc merged 1 commit into
mainfrom
sit-attribution-ip-hardening
Jun 4, 2026
Merged

fix(attribution): ignore shared/NAT IPs in fingerprint matching + CDN client-IP header#26
onamfc merged 1 commit into
mainfrom
sit-attribution-ip-hardening

Conversation

@onamfc
Copy link
Copy Markdown
Member

@onamfc onamfc commented Jun 4, 2026

Why

Cross-tenant install mis-attribution: coarse, org-blind fingerprint matching attributed one app's installs to a different tenant's link. Because the events page scopes in-app events by the matched link's org, that surfaced one app's in-app events under another workspace's feed (observed: a baby-store app's events under an unrelated customer's link, all from shared 100.64.0.x CGNAT IPs).

Changes

  1. Shared-IP filtercalculateConfidenceScore no longer awards the IP score when either IP is in a shared/non-routable range (CGNAT 100.64.0.0/10, RFC1918, loopback, link-local, IPv6 ULA/link-local). Without the IP score such installs top out at 60 and no longer reach the 70 threshold. New exported isAttributableIp().

  2. CDN client IPgetClientIp honors TRUSTED_CLIENT_IP_HEADER (e.g. cf-connecting-ip) so installs behind Cloudflare are fingerprinted on the real device IP, not a CDN/NAT hop. Opt-in; default behavior unchanged.

Compatibility / risk

  • Backward-compatible: both behaviors are additive/opt-in. Public-IP attribution is unchanged.
  • Existing /24-match tests repointed from private to public IPs (their intent was to verify /24 matching, which now correctly requires routable IPs).
  • Full suite green (125 tests), including new cases for the exact CGNAT scenario.

…port CDN client-IP header

Two root-cause fixes for cross-tenant install mis-attribution:

1. Shared-IP filter: calculateConfidenceScore no longer awards the IP score
   when either IP is in a shared/non-routable range (CGNAT 100.64.0.0/10,
   RFC1918, loopback, link-local, IPv6 ULA/link-local). Those IPs don't
   identify a single device, so unrelated users behind the same NAT could
   collide on IP (40pts) + UA (30pts) = the 70 threshold and cross orgs.
   Without the IP score such installs top out at 60 and no longer match.
   New exported isAttributableIp() with tests.

2. getClientIp honors a configurable authoritative client-IP header
   (TRUSTED_CLIENT_IP_HEADER, e.g. cf-connecting-ip) so installs behind
   Cloudflare are fingerprinted on the real device IP instead of a CDN/NAT
   hop. Opt-in; default behavior unchanged.

Existing /24-match tests repointed from private to public IPs (their intent
was to verify /24 matching, which now correctly requires routable IPs).
Full suite green (125 tests).
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@onamfc onamfc merged commit 58684e1 into main Jun 4, 2026
12 of 13 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant