|
| 1 | +# sdk-react-native |
| 2 | + |
| 3 | +## What this codebase does |
| 4 | + |
| 5 | +`@formo/analytics-react-native` — a client-side analytics SDK (npm |
| 6 | +library) embedded into third-party React Native dApps. It tracks wallet |
| 7 | +events (connect / disconnect / signature / transaction / chain), screen |
| 8 | +views, and custom events, batches them in `EventQueue`, and POSTs them |
| 9 | +to `https://events.formo.so/v0/raw_events`. Optional Wagmi integration |
| 10 | +(`WagmiEventHandler`) auto-captures wallet activity. There is **no |
| 11 | +server component** in this repo — it is shipped code that runs inside |
| 12 | +other people's apps. Classic web vulns (SQLi, SSRF, server authz) mostly |
| 13 | +do not apply; data-handling and supply-chain concerns dominate. |
| 14 | + |
| 15 | +## Auth shape |
| 16 | + |
| 17 | +There is no user/session auth. The only credential is the **`writeKey`** |
| 18 | +— a *public, write-only* ingest key intentionally bundled into client |
| 19 | +apps and sent as `Authorization: Bearer ${writeKey}` via |
| 20 | +`EVENTS_API_REQUEST_HEADER` (`constants/config.ts`). Treat it as |
| 21 | +non-secret. The relevant gates instead are: |
| 22 | + |
| 23 | +- `hasOptedOutTracking()` / `CONSENT_OPT_OUT_KEY` + `setConsentFlag` / |
| 24 | + `getConsentFlag` / `removeConsentFlag` (`lib/consent`). |
| 25 | +- `isBlockedAddress` / `BLOCKED_ADDRESSES` (`utils/address.ts`). |
| 26 | +- `validateAddress` (EVM checksum + Solana) before an address is |
| 27 | + attached to any event. |
| 28 | + |
| 29 | +## Threat model |
| 30 | + |
| 31 | +Highest impact: (1) PII / sensitive-data exfiltration — the SDK collects |
| 32 | +wallet addresses, the **raw message text being signed**, device info, |
| 33 | +deep-link URLs, and UTM/referrer attribution; anything that logs, |
| 34 | +persists, or ships a private key or full signature secret is critical. |
| 35 | +(2) Consent bypass — events generated or flushed after the user opted |
| 36 | +out. (3) Supply-chain / untrusted input — host-app-supplied event |
| 37 | +`properties` and deep-link URLs flow into the outbound payload. |
| 38 | + |
| 39 | +## Project-specific patterns to flag |
| 40 | + |
| 41 | +- **Signature capture leaking secrets.** `signature()` / |
| 42 | + `createSignatureEvent` (`EventFactory`) and `WagmiEventHandler` |
| 43 | + mutation tracking store `message` + optional `signatureHash` only. |
| 44 | + Flag any path that captures a private key, mnemonic, or the produced |
| 45 | + signature itself beyond `signatureHash`. |
| 46 | +- **Consent-gate bypass.** Every new event entry point must check |
| 47 | + opt-out before `eventQueue.enqueue`, and opt-out must call |
| 48 | + `eventQueue.clear()`. Flag new track paths that skip the consent |
| 49 | + check. |
| 50 | +- **Unvalidated address pass-through.** Addresses attached to events |
| 51 | + must go through `validateAddress` / `isBlockedAddress`; flag raw |
| 52 | + user/wallet address used directly in a payload. |
| 53 | +- **Untrusted properties / deep-link URLs** merged into event payloads |
| 54 | + (`parseTrafficSource`, `startDeepLinkCapture`, custom `track` |
| 55 | + properties) without sanitization — prototype-pollution or oversized |
| 56 | + payload risk. |
| 57 | + |
| 58 | +## Known false-positives |
| 59 | + |
| 60 | +- `writeKey` in the `Bearer` header / bundled in client config — public |
| 61 | + write-only key by design, **not** a leaked secret. |
| 62 | +- `options.apiHost` overriding the ingest endpoint — intended |
| 63 | + self-host/proxy support; not SSRF (runs in the host app's own |
| 64 | + process, configured by that app's developer). |
| 65 | +- `generateUUID()` using `Math.random()` (`utils/hash.ts`) — analytics |
| 66 | + dedup/anon IDs, not security tokens. Acceptable. |
| 67 | +- **Minute-rounded `message_id` deduplication** (`generateMessageId` → |
| 68 | + `toDateHourMinute`, `lib/event/EventQueue.ts`) — intentional and |
| 69 | + **by design**. This is the *canonical cross-SDK + backend dedup |
| 70 | + contract* shared with `@formo/analytics` (web) and the |
| 71 | + `events.formo.so` ingest pipeline; content-identical events colliding |
| 72 | + within the same wall-clock minute is expected behavior, not a bug. Do |
| 73 | + **not** flag or "fix" this — including the BUG finding |
| 74 | + `other-deduplication-data-loss`. The `message_id` contract may only |
| 75 | + change via coordinated web-SDK + backend + RN sign-off, never as an |
| 76 | + SDK-local patch. |
| 77 | +- Unencrypted `AsyncStorage` / `MemoryStorage` fallback — only stores |
| 78 | + anon id + consent flag, non-sensitive by design. |
| 79 | +- Hardcoded `SOLANA_SYSTEM_ADDRESSES` / `BLOCKED_ADDRESSES` — public |
| 80 | + constants, not secrets. |
| 81 | +- Anything under `src/__tests__/` — fixtures and mocks. |
0 commit comments