This is a fully-offline, single-device app with no accounts, no server, and no network calls (except font loading, removable — see DEPLOYMENT.md step 4). There is no data worth stealing beyond card-game scores, and nothing is transmitted anywhere. The realistic risks are therefore:
- Corrupted or tampered persisted data crashing the app — interrupted writes, backup restores, or manual tampering via device backup tools. The failure mode to prevent is a crash loop on launch that permanently bricks the app.
- Hostile input — pathological player names (unbounded length, control characters) bloating storage or corrupting the UI.
- Webview compromise — script injection into the WKWebView.
- Supply chain — vulnerable npm dependencies.
| Risk | Control | Where |
|---|---|---|
| Corrupt storage → crash loop | Every persisted key is parsed with safeJsonParse (never throws) and shape-validated; corrupt raw data is quarantined under a corrupt: key for post-mortem, and that key alone starts fresh |
src/lib/validation.js, load path in src/hooks/useJudgementGame.js |
| Tampered data with impossible values | Validators repair (clamp negatives, cap correctBids ≤ roundsPlayed) or reject structurally-unusable games | validateStats, validateGame |
| Prototype pollution via stored JSON | __proto__/constructor/prototype keys dropped during validation |
validateStats, validateMasteredTips |
| Pathological player names | Sanitized: control/zero-width chars stripped, whitespace collapsed, 40-char cap, 30-player roster cap | sanitizePlayerName, addPlayer |
| Out-of-range bid values | Clamped to integers in [0, cardCount] at the entry point |
clampBidValue, updateBid/updateTricks |
| Script injection (XSS) | No dangerouslySetInnerHTML, no eval, React auto-escaping throughout; CSP forbids remote scripts, connections, frames, and objects |
index.html CSP meta tag |
| Supply chain | npm audit --omit=dev --audit-level=high gates CI on runtime dependencies (the only code shipped to users); lockfile committed |
.github/workflows/ci.yml |
| Diagnostics leaking data | The logger records keys and error messages, never full storage values; the diagnostics export goes only to the user's own clipboard, on their explicit tap | src/lib/logger.js, ErrorBoundary |
The audit gate is scoped to runtime dependencies (--omit=dev) on purpose.
The shipped bundle contains only React and the Capacitor runtime; the build
and test tooling (Vite, esbuild, the Capacitor CLI) never runs on a user's
device. Their advisories — e.g. the esbuild dev-server request issue — apply
to a developer's local machine, not to the App Store binary, so they are
tracked but not treated as release blockers.
- Encryption at rest: the data is card-game scores on the user's own device. iOS file-level protection already applies to the app container. Adding app-layer encryption would add key-management complexity with no meaningful benefit.
- Authentication: single-device, shared-around-the-table by design.
- Jailbreak detection / anti-tamper: a user modifying their own scorepad harms only their own leaderboard.
If you find a vulnerability, open a GitHub issue (there is no sensitive data category here that warrants private disclosure).
Two future changes would expand this threat model, and each has a marked integration point:
- Enabling crash reporting (Sentry): data leaves the device. Add the
ingest domain to the CSP
connect-src, and update the App Store privacy declaration from "Data Not Collected". Hook:setErrorReporterinsrc/main.jsx. - Any sync/multiplayer feature: introduces a server and real authn/authz questions. Revisit this document entirely before building.