Cross-platform Expo / React Native client for the A-List creator×brand offers platform (UAE). Named "android" but builds iOS, Android & web from a single TypeScript codebase.
expo-secure-store (not AsyncStorage) for tokens. Solid foundation.eas.json ships every build profile (including production) with EXPO_PUBLIC_API_BASE_URL=https://dev-api.alist.ae/v1. Almost certainly a configuration bug — needs verification before next release.package.json has no test script, no Jest config, and there is no bitbucket-pipelines.yml or GitHub Actions workflow. All quality gating is manual.shared/utils/request.ts exists (timeouts, 401→refresh, error normalising) but ~47 of the 47 raw fetch() calls across feature api/ files bypass it. Inconsistent error handling and no retry logic in those paths.shared/utils/storage.ts and several auth flows use catch { /* silent */ }. Combined with the no-Sentry gap, real failures are invisible.core/providers/StoreProvider.tsx and features/offers/store/offersStore.tsx are byte-identical; constants/HomeConstants.ts and features/offers/constants/HomeConstants.ts also duplicate.| Category | Technology | Version | Notes |
|---|---|---|---|
| Runtime | React Native | 0.81.4 | newArchEnabled: true in app.json |
| Framework | Expo | ^54.0.7 | Current. Includes expo-dev-client, splash, font, secure-store, notifications, video plugins |
| Routing | Expo Router | ~6.0.7 | File-based. experiments.typedRoutes enabled. Tabs + Stack groups |
| Language | TypeScript | ~5.9.2 | Strict on. ~120 explicit any annotations remain |
| State (client) | Zustand | ^5.0.8 | authStore, uiStore in core/state/ |
| State (server) | @tanstack/react-query | ^5.90.2 | staleTime 30 min, gcTime 60 min, refetchOnMount: false |
| Persistence | expo-secure-store | ~15.0.7 | Tokens + form/profile data. Web fallback: localStorage |
| Push | expo-notifications + custom FCM | ^0.32.11 | Token refresh repurposes FCM register endpoint to mint fresh JWT |
| Support chat | @intercom/intercom-react-native | ^9.1.1 | Tab press intercepted in app/main/_layout.tsx |
| Auth providers | @react-native-google-signin/google-signin | ^16.1.2 | Google client IDs in app.json & eas.json |
| Lint | eslint-config-expo (flat) | ~10.0.0 | Default rules only. No custom rules. |
| Tests | — | — | None. No Jest, RNTL, Detox, or Maestro. |
| CI | — | — | No bitbucket-pipelines.yml. Builds via EAS CLI. |
| Crash reporting | — | — | No Sentry / Crashlytics / Bugsnag. |
Feature-sliced Expo Router app. app/ defines routes (file-based), features/<name>/ holds the implementation per domain (api / components / hooks / queries), shared/ hosts cross-feature primitives, core/ hosts Zustand stores and the React Query provider, and config/environment.ts centralises env-driven settings with baked-in dev fallbacks. The app boots through app/_layout.tsx (providers + Intercom) into app/index.tsx (auth gate → /main/home).
This assessment is a manual, evidence-driven source review of the entire alist-android tree (excluding node_modules/.git), cross-checked against committed configuration and git history. Every finding below was confirmed by reading the cited file and line, decoding literals, or proving absence via repo-wide grep — scanner output was de-duplicated and merged into canonical findings. Severities map to OWASP risk and CWE classes with CVSS 3.1 vectors; the "confidence" tag distinguishes client-side-confirmed issues from those whose full exploitability depends on unverified backend behaviour.
mobile-storage · mobile-api-auth · mobile-transport · mobile-platform · supply-chain-frontend
alist-android ships a privileged Intercom Access Token (tok: prefix, server-grade REST credential) committed to git and inlined into every distributed build via the EXPO_PUBLIC_ prefix, giving anyone with the binary or repo read access the ability to enumerate and exfiltrate the entire A-List creator contact list (PII) and read/write support conversations — a direct, network-exploitable breach requiring no device access. The authentication design compounds this: a stolen JWT can be re-minted indefinitely by re-registering an FCM device token (no real refresh/revocation), logout never revokes server-side, the 4-digit OTP has no client throttling, and the OTP value is both returned in the API response and surfaced in a Toast whose dev-gate evaluates true in production (all six EAS profiles point at dev-api.alist.ae). Transport is entirely unpinned, and a server-controlled URL is loaded into a JavaScript-enabled WebView with no origin allowlist, so an on-path attacker who can trust a CA can steal tokens/OTPs and inject phishing content into the trusted app chrome. Supply-chain hygiene is broken — no lockfile (deleted and gitignored), 60+ floating version ranges, and zero CI/SCA — making malicious or vulnerable dependencies invisible until they ship. The realistic worst case is full creator-account takeover plus a platform-wide PII breach, so overall risk is critical.
eas.json/app.json/google-services.json (commit e84bb6d) — matches the May 2026 audit's ubiquitous-secrets-in-git theme; rotate-before-refactor applies.production) point at https://dev-api.alist.ae/v1 — this single misconfiguration amplifies the OTP-leak dev-gate, makes the production app trust the lower-trust dev backend for WebView/legal URLs, and undermines every transport finding.user_id in OTP verify, FCM-endpoint token re-minting, no server-side logout revocation, JWT exp never checked — a systemic auth-design weakness to correlate with the backend repos.catch {} / // Silently handle error) with no telemetry — hides tampering and failed logouts, a repo-wide convention noted in CLAUDE.md.| Category | Status | Notes |
|---|---|---|
| M1: Improper Credential Usage | Vuln | AA-01 privileged Intercom Access Token committed and bundled; AA-14 other keys committed in config |
| M2: Inadequate Supply Chain Security | Vuln | AA-05 no lockfile, AA-08 no CI/SCA, AA-15 unmaintained render-html, AA-14 keys-in-repo |
| M3: Insecure Authentication/Authorization | Vuln | AA-02 FCM token re-mint, AA-03 client user_id, AA-07 4-digit OTP, AA-09 no server logout, AA-18 OAuth |
| M4: Insufficient Input/Output Validation | Vuln | AA-06 WebView no origin gating, AA-15 unsanitized HTML, AA-21 unvalidated push offerId |
| M5: Insecure Communication | Vuln | AA-04 no TLS pinning, AA-12 cleartext http accepted + token attached, no ATS/cleartext config |
| M6: Inadequate Privacy Controls | Vuln | AA-11 PII/OAuth code logged to console; AA-01 enables bulk PII export; AA-17 user enumeration |
| M7: Insufficient Binary Protections | Partial | Managed Expo build; no obfuscation/anti-tamper, but no native code reviewed — EXPO_PUBLIC_ secrets trivially extractable from bundle (AA-01) |
| M8: Security Misconfiguration | Vuln | All 6 EAS profiles point at dev-api in production; WebView insecure defaults (AA-06); no cleartext/ATS hardening (AA-12) |
| M9: Insecure Data Storage | Vuln | AA-10 OTP in response/toast, AA-13 no screen-capture protection, AA-16 silent storage errors, AA-19 clipboard exposure; SecureStore otherwise correctly used |
| M10: Insufficient Cryptography | Vuln | AA-20 Math.random() for device tokens; AA-18 guessable OAuth state nonce |
| API1:2023 Broken Object Level Authorization | Vuln | AA-03 OTP verify trusts client-supplied user_id |
| API2:2023 Broken Authentication | Vuln | AA-02, AA-07, AA-09, AA-10, AA-17 — token lifecycle and OTP weaknesses across the auth surface |
| A02:2021 Cryptographic Failures | Vuln | AA-12 token eligible to traverse cleartext; AA-04 no pinning |
| A05:2021 Security Misconfiguration | Vuln | AA-01/AA-14 secrets committed; dev API in prod build profiles |
| A06:2021 Vulnerable and Outdated Components | Vuln | AA-05 no lockfile, AA-08 no SCA, AA-15 archived render-html, version-skewed/dead deps |
| A09:2021 Security Logging & Monitoring Failures | Vuln | AA-11 sensitive console logging in prod; AA-16 silent error swallowing; no crash reporting/telemetry |
Headline counts above reflect the verified summary (P0 1 / P1 5 / P2 6 / P3 6). All 21 confirmed findings are detailed below in P0→P3 order; a handful merge multiple overlapping scanner findings into one canonical entry, which is why the per-card tally runs slightly higher than the summary counts.
The EXPO_PUBLIC_INTERCOM_TOKEN committed across all six EAS build profiles base64-decodes to tok:40e437cb_0ad5_4d76_9a04_4efdec5c62d7:1:0, an Intercom Access Token used to authenticate against the Intercom REST API (api.intercom.io). Unlike the public Intercom SDK app keys (android_sdk-/ios_sdk-), an Access Token is a privileged server-grade credential that authorizes programmatic read/write to the workspace's contacts, conversations, companies and admin data. It is hardcoded in plaintext in git-tracked eas.json, read by config/environment.ts, and — because of the EXPO_PUBLIC_ prefix — inlined into the shipped JavaScript bundle.
tok: Access Token, then issues GET https://api.intercom.io/contacts with Authorization: Bearer <token> to page through and exfiltrate every creator contact, and POSTs replies to impersonate A-List support.An attacker who extracts the token (from any shipped binary, the web bundle, or git history) can call the Intercom REST API to enumerate and export the full A-List creator contact list (names, emails, phone numbers — PII), read and write support conversations, impersonate the workspace in messaging, and tamper with CRM data. This is a direct, network-exploitable PII breach affecting all platform users with no device access required. The token is duplicated across 6 profiles, so rotation requires coordinated edits.
EXPO_PUBLIC_INTERCOM_TOKEN from eas.json and config/environment.ts entirely — a REST Access Token must never live in the mobile client; any Intercom REST calls belong on the backend. (3) Purge it from git history (git-filter-repo/BFG) and force-push with team coordination. (4) For client-side Intercom, use only the public SDK app keys plus a server-computed Identity Verification HMAC handed to the app per-user. (5) Add gitleaks pre-commit + CI scanning with a custom rule for tok: Intercom tokens.Confirmed by base64-decoding the literal and reading the read path in environment.ts:56. Three scanner findings (mobile_storage P0, mobile_api_auth P1 'Identity Verification secret', supply_chain_fe P2 'CI secrets committed') all describe this same tok: value; merged here. The tok: prefix is Intercom's Access Token, the higher-impact interpretation, so kept at P0.
The app has no real refresh-token flow. shared/utils/tokenRefresh.ts re-calls the push device-registration endpoint POST /user/fcm_token (authenticated solely by the existing access token) and treats the JWT returned in that response as a fresh access token, persisting it to SecureStore. This is wired into the default request path (retryOn401:true). Any holder of one valid access token can transparently and indefinitely obtain new JWTs by registering an arbitrary FCM device token — no re-authentication, no proof-of-possession, no rotation. Combined with shouldRefreshToken() always false and exp never decoded, practical token lifetime is unbounded.
POST /user/fcm_token with a random device_token; the backend returns a new JWT; the attacker repeats indefinitely, maintaining access long after the original token's intended expiry and even after the victim logs out (see AA-09).A stolen or intercepted access token grants effectively permanent account access: the attacker keeps it alive forever by re-registering a random device token. Token rotation/expiry — the primary mitigation for token theft — is neutralized, and the client exposes no way to invalidate a leaked token short of a backend action it never triggers.
/user/fcm_token as an auth-refresh oracle and do not return an access token from device registration. Decode the JWT client-side to honour exp and refresh only within a short window. On the backend, bind tokens to a device/session, enforce absolute and idle lifetimes, and check a revocation list on every request.Confirmed end-to-end: the FCM endpoint returns data.token, refreshAuthToken persists it, and request.ts auto-invokes it on 401. CLAUDE.md itself flags this as 'unusual and worth confirming with backend'.
The login OTP verify request to POST /login/verify-otpv2 carries three independently attacker-influenceable values: email, otp, and a distinct user_id (replayed from the requestOtp response). Because identity is conveyed as a separate user_id parameter rather than being derived strictly server-side from the email+OTP pair the server issued, the flow is vulnerable to OTP/identity-binding confusion: if the backend keys the OTP lookup or issued session on the client-supplied user_id (or fails to enforce that email+otp+user_id all reference the same record), an attacker can request an OTP for their own account and substitute a victim user_id/email to have a valid token minted for the victim. Numeric IDs are sequential and client-exposed, making enumeration trivial.
/login/verify-otpv2 with {email: attacker, otp: attacker_otp, user_id: <victim sequential id>}; if the server binds the session to user_id, it returns a JWT for the victim account.Potential full account takeover of arbitrary users by manipulating user_id (and/or email) in the verify-otp request, bypassing the OTP second factor. Even absent full takeover it weakens OTP binding and enables targeted abuse.
user_id from the verify-otp request; the server must resolve the user solely from the email it issued the OTP to plus the OTP value, rejecting any mismatch. Bind each OTP to a single immutable account record server-side. After verification the client should treat the server-returned identity as authoritative and ignore the locally-held user_id. Use opaque, non-sequential identifiers.Client-side contract confirmed (separate user_id param accepted and forwarded; sequential id exposed). Marked 'likely' because full exploitability depends on unverified backend binding logic — but the client design is a textbook BOLA smell.
Every network call — the centralized request.ts wrapper and all raw fetch() call sites — relies solely on the device OS trust store to validate the server certificate. There is no certificate pinning, public-key pinning, or Certificate Transparency enforcement anywhere in the codebase, and because this is a pure managed Expo build with no native config, even the baseline of excluding user-installed CAs is not asserted.
An on-path attacker who can get a CA trusted on the victim's device (rogue Wi-Fi captive portal, corporate MDM, malware-installed user CA, or a compromised public CA) can decrypt and modify all traffic: steal the Bearer JWT (account takeover, amplified by AA-02), capture the email+OTP during login, alter offer-redemption/venue-verification flows, and harvest PII.
*.alist.ae. For managed Expo: add expo-build-properties and supply an Android network_security_config.xml with <pin-set> entries plus an iOS pinning approach (e.g. TrustKit via config plugin), or route ALL calls through request.ts and pin in the fetch layer. Pin leaf/intermediate SPKI with at least one backup pin and an expiry, and pair with Certificate Transparency. Coordinate pin rotation with backend cert renewal.Confirmed absence by grep and dependency/plugin review. Severity P1 because it is the precondition that elevates AA-02, AA-03, AA-05, AA-07 and the WebView injection (AA-06).
The repository ships no dependency lockfile. A package-lock.json existed at the first commit but was deleted on 2026-05-14 and added to .gitignore. With ~60 of ~81 declared dependencies using caret/tilde ranges, every npm install (the documented and only install path, including on EAS Build workers) re-resolves the full graph from the registry at build time with no recorded resolved versions and no integrity hashes. This is the canonical precondition for a supply-chain attack and makes builds non-reproducible.
An attacker who compromises any package in the unpinned transitive tree — or publishes a malicious patch/minor within an allowed range — gets code executed on developer machines and EAS Build infrastructure, bundled into the shipped app, invisibly until a build behaves maliciously. Builds are irreproducible, undermining incident forensics.
package-lock.json (or yarn.lock) and remove it from .gitignore. Switch install to npm ci everywhere (dev docs + EAS build steps). Consider save-exact=true for security-sensitive direct deps. Add a CI gate that fails if the lockfile is missing or drifts from package.json.Confirmed lockfile deletion and .gitignore exclusion via git history and file listing. The historical stale-lockfile detail (SDK 53/RN 0.79) is folded into this finding as corroborating evidence.
The only in-app WebView renders a URL supplied by the backend legal endpoints (terms/privacy) using react-native-webview defaults: JavaScript enabled, no originWhitelist, no onShouldStartLoadWithRequest scheme/host gating, setSupportMultipleWindows left at default true. The URL originates from unauthenticated GET /user-terms and /privacy-policy (and a SecureStore cache) on the dev-pointed production backend. Anyone who can influence that response — compromised dev API, MITM on the unpinned channel (AA-04), or an open-redirect on the served page — controls a fully JS-enabled WebView inside the authenticated app chrome with no navigation gating.
/user-terms response with https://evil/phish; the user taps 'Terms', the WebView loads and runs attacker JS that renders a fake 'session expired, re-login' form harvesting credentials and OTP.An attacker who influences the legal-URL response gets arbitrary JavaScript execution in an in-app WebView with no origin restrictions, enabling credential/OTP phishing pages indistinguishable from the app, redirect chains to attacker origins, and cleartext content loads. No JS bridge limits direct native RCE, but the content-injection primitive inside trusted chrome is real.
javaScriptEnabled={false} (terms/privacy are static), add originWhitelist={['https://*.alist.ae']}, implement onShouldStartLoadWithRequest to allow only expected https hosts and reject custom schemes, set setSupportMultipleWindows={false} and allowFileAccess={false}/allowFileAccessFromFileURLs={false}. Validate scheme/host before assigning url. Serve legal URLs over an authenticated/integrity-checked channel and fix the production EAS profile to use the prod API. Resolving AA-04 removes the MITM precondition.Two scanner findings (mobile_platform P1 insecure defaults; mobile_transport P2 server-controlled URL no validation) describe the same WebView; merged. Confirmed by reading InAppBrowser.tsx and both legal.ts files (unauthenticated GET). Kept P1 per the higher scanner rating.
The login second factor is a 4-digit numeric OTP (keyspace 10,000). The client imposes no attempt limiting, no exponential backoff, no lockout after repeated failures, and no resend cooldown, and the verify endpoint is a plain POST with no nonce/captcha, so it is directly scriptable. Rate-limiting must ultimately be server-side, but the complete absence of any client control combined with the tiny keyspace means that if backend per-account attempt caps or OTP TTLs are weak, the code is brute-forceable in seconds.
POST /login/verify-otpv2 iterating 0000-9999; with no client/anti-automation friction the only barrier is unverified server throttling.If server-side throttling is weak or absent, an attacker can brute-force the 4-digit OTP for any email and authenticate as that user, fully bypassing the OTP factor.
Client-side absence of all anti-automation confirmed; 4-digit length confirmed in loginHelpers constants. 'likely' because real-world brute-forceability hinges on unverified server throttling, but the design is materially weaker than the 6-digit standard.
The project has no CI pipeline and no dependency-scanning automation (no Snyk, Dependabot, Renovate, npm audit, or SBOM step). Combined with the missing lockfile (AA-05), there is no automated point at which a vulnerable, outdated, or malicious dependency is detected before it reaches a build artifact, and new CVEs against the ~80 declared packages are never surfaced. Releases are cut by running EAS Build manually from developer machines.
Vulnerable or compromised dependencies enter shipped builds undetected; there is no quality gate to block a known-CVE or malicious version. The manual, machine-local release process widens the trust boundary to every contributor workstation, and mean-time-to-detect for a supply-chain compromise is effectively unbounded.
npm ci (lockfile-enforced), npm audit --audit-level=high or snyk test --severity-threshold=high, lint, typecheck — failing on high/critical. Add Dependabot/Renovate. Generate and scan an SBOM (syft/grype) per release. Enforce branch protection and move EAS production builds into the pipeline.Confirmed absence of all CI/SCA config. Distinct from AA-05 (integrity anchoring) — this is the missing detection/gate layer; both are systemic.
logout() and the 401 force-logout path only delete the locally-stored token and clear local state; they never ask the backend to invalidate the JWT. The only server call during logout is unregisterFCMToken (DELETE /user/fcm_token), which removes the push device, not the auth session, and its failure is swallowed. The JWT therefore remains valid server-side after logout, so any copy exfiltrated earlier keeps authenticating until natural expiry — and per AA-02 expiry can be circumvented.
Stolen or cached tokens stay usable after the user logs out, including on shared/lost devices. Users cannot truly terminate a compromised session from the app.
POST /auth/logout) that blocklists the JWT/refresh token server-side, invoked from both logout() and handleUnauthorizedSession(). Maintain a server-side blocklist or short-lived access tokens with revocable refresh tokens. Stop swallowing the revocation result.Confirmed by reading authStore.logout, clearAllAuthData, session.ts, and grepping for a logout endpoint (none). Compounds AA-02.
The OTP returned by the backend is rendered into a user-visible Toast. The 'dev only' guard is Environment.api.baseUrl.includes('dev-'), but every EAS profile including production points at dev-api.alist.ae, so the guard is true in production and the OTP is displayed to whoever operates the app. More fundamentally, the backend returning the OTP in the API response at all (data.data?.otp) means the second factor is recoverable by anyone who can read the HTTP response, collapsing the OTP's out-of-band value.
/login/with-otp JSON containing the OTP; or, more simply, the production app itself shows the OTP in a success toast because the dev-gate matches the dev-api host, letting anyone holding the unlocked device complete login.The OTP factor provides little protection if the code is returned in the response and surfaced in the UI; an attacker observing traffic (AA-04) or shoulder-surfing the toast obtains the code directly, and the mis-scoped dev gate leaks OTPs to all production users.
__DEV__), not a runtime substring match on the API host. Fix the production EAS profile to target the prod API.Confirmed all three toast sites and the dev-gate substring logic, plus that all eas.json profiles use dev-api (so the gate fires in prod). Merges the three OTP-leak scanner findings. Kept P2.
Several API modules log full response/request bodies, PII, and OAuth artifacts to console with no __DEV__ guard, so they run in production. On Android, console.* is forwarded to logcat, readable via ADB during support, by some OEM/MDM logging, and by other apps with READ_LOGS on legacy devices. fetchUserProfile/fetchSocialMediaProfile dump the full profile object; connectYoutube logs the OAuth authorization code and state; auth.ts logs login endpoint URLs and truncated server error bodies.
adb logcat and captures the dumped profile JSON and the YouTube OAuth code/state, then replays the code against /connect-youtube.PII and OAuth authorization codes for creators are written to device logs in production. An attacker with local log access (malicious app on legacy Android, physical/ADB access, or log aggregation) can harvest PII and potentially replay an OAuth authorization code before it is exchanged.
console.log calls from profile.ts, signup.ts, and auth.ts. Introduce a central logger that no-ops or redacts when !__DEV__, never logging full responses, OAuth codes, tokens, or PII. Add babel-plugin-transform-remove-console for release builds and an ESLint no-console rule for production paths.Confirmed all cited lines. Merges the mobile_storage P2 PII-logging and mobile_transport P3 auth-URL-logging findings (same root cause).
buildUrl() will issue requests to any caller-supplied absolute URL including plaintext http://, and withAuthHeaders attaches the user's Bearer token regardless of scheme or host. There is no https-only enforcement and no host allowlist, so a misconfiguration, a server-returned URL fed into get()/post(), or a downgrade can transmit the token in cleartext or to an unintended host. The app declares no Android cleartext restriction or iOS ATS config, leaving only the platform default, and IMAGE_BASE_URL is derived by brittle string replacement that inherits any misconfiguration.
http:// scheme (or an off-domain host) is passed to get()/post(); the helper dutifully attaches the Bearer token, and an on-path attacker reads it from the cleartext request or the unintended origin logs it.Risk of access-token disclosure over cleartext on hostile Wi-Fi, or token leakage to an attacker-controlled host if any URL fed to the request helper is influenced by remote data. Tokens transit in plaintext, enabling session hijacking.
buildUrl, reject non-https schemes and enforce an allowlist of trusted alist.ae hosts before attaching credentials; never attach Authorization to off-allowlist origins. Add expo-build-properties: Android networkSecurityConfig with cleartextTrafficPermitted=false and strict iOS ATS. Validate EXPO_PUBLIC_API_BASE_URL at startup (must be https + alist.ae). Derive IMAGE_BASE_URL from a dedicated validated env var.Confirmed buildUrl/withAuthHeaders code and absence of cleartext config. Merges the mobile_api_auth P2 and mobile_transport P2 cleartext findings (same buildUrl root cause).
The app displays security-relevant data on screen — login OTP codes, Instagram/TikTok ownership-verification codes, and redeemable voucher codes — with no screen-capture protection (no Android FLAG_SECURE, no expo-screen-capture, no iOS background blur). These values are captured in OS screenshots, the Android Recents/app-switcher thumbnail, and any screen recording including malware with MediaProjection/accessibility capabilities.
Sensitive auth/verification material leaks via automatic Recents thumbnails, cloud-synced screenshots, or screen-recording malware, enabling account-verification spoofing or voucher theft. Bounded to local/physical or on-device-malware adversaries.
expo-screen-capture and call usePreventScreenCapture() on OTP, social-verification, and voucher screens. On Android set the secure window flag for those activities; on iOS blur sensitive content when the app enters the inactive/background state.Confirmed absence by grep and dependency review; sensitive-render sites confirmed.
Multiple third-party identifiers are committed: Intercom mobile SDK keys, the Firebase/Google API key in a tracked google-services.json, Google OAuth client IDs, and the Android signing certificate SHA-1. These categories are by design embeddable in mobile clients (SDK keys, OAuth client IDs, and Firebase API keys are documented non-secret, access being controlled server-side), so direct impact is low — but their safety depends entirely on server-side hardening (Firebase Security Rules and GCP API/application restrictions), committing them centralizes exposure and complicates rotation, and they must be distinguished from the genuinely-privileged Intercom Access Token in AA-01.
Low direct impact in isolation. An unrestricted AIzaSy... key can be abused for billed Google API calls or service enumeration if it lacks application + API restrictions; Intercom SDK keys allow initiating anonymous conversations. Repo read access (contractor, leaked clone, mirror) yields the full integration key set in one place, and the six-profile duplication makes rotation error-prone.
AIzaSy... key has Application restrictions (Android package + SHA-1 / iOS bundle) AND API restrictions. Keep google-services.json out of source control where practical (EAS file-based secrets / app.config.js injection) and add it to .gitignore. Move Intercom SDK keys out of literal eas.json into EAS encrypted env vars. Document which values are intentionally public so reviewers can distinguish them from AA-01. Add gitleaks in CI.Confirmed all literals and that google-services.json is tracked (committed e84bb6d, not gitignored). Distinct from AA-01 because these are the non-privileged class; merges the mobile_storage P2 SDK-keys finding and the supply_chain P2 committed-config finding (excluding the tok: token, which is AA-01).
react-native-render-html@^6.3.4 is an abandoned/archived library used pervasively to render HTML from the API (offer/voucher descriptions, deliverable rules, voucher redemption/booking text). It renders to native RN components (so <script> does not execute — not classic web XSS/RCE), but components configure <a> tagsStyles without restricting link href schemes or sanitizing, so backend-authored (and where any field is merchant/user-influenced, attacker-influenced) HTML can present styled hyperlinks to arbitrary URIs (alistapplication://, javascript:, phishing https) and arbitrary inline markup. Because the package is unmaintained, any CVE in it or its bundled parser tree (ReDoS/parser-confusion classes historically seen in nth-check/css-select/htmlparser2) will never be patched in place, and the floating ^6.3.4 range cannot upgrade to a fixed major because none will be published.
No security patches will ever arrive for a component that processes remote, attacker-influenceable HTML on every offer/voucher view, across the core browse/redeem flows (high blast radius). A future or existing parser CVE (ReDoS/crash/link abuse) is permanently unpatchable, and unsanitized anchors enable in-app link phishing. The dev-pointed production API (AA-06 context) weakens the 'trusted backend' assumption.
react-native-render-html with a maintained alternative, or render via a hardened WebView with a strict allowlist + sanitization, or have the backend deliver pre-sanitized constrained markup. Gate anchor presses with renderersProps.a.onPress validating scheme/host (only https alist.ae) before Linking.openURL. Sanitize description server-side. Track the dependency in SCA (AA-08) so any disclosed parser CVE is flagged.Confirmed 8 call sites and server-data binding by grep + reading OfferContent.tsx. Merges the supply_chain P2 unmaintained-dep finding and the mobile_platform P3 unsanitized-HTML finding (same component/library); kept P2.
Every function in the SecureStore-backed storage layer wraps its body in a try/catch that silently discards the error. SecureStore itself is correctly used for encrypted-at-rest storage, but silent swallowing means a failed clearToken/clearAllAuthData during logout (Keychain/Keystore error) leaves the auth token and PII resident while the UI shows a successful logout. It also hides integrity/decryption failures that could indicate tampering, with no telemetry to surface them.
clearAllAuthData throws on a Keystore error; the catch swallows it; the UI navigates to the logged-out screen but the JWT remains in SecureStore; the next holder of the device relaunches and is silently re-authenticated.On a shared or lost device, an incomplete logout can leave a valid bearer token and profile PII in SecureStore with the user believing they signed out, enabling session continuation by the next device holder. Integrity-affecting, low likelihood, compounded by the absence of crash/error reporting and by AA-09 (no server revocation).
Confirmed the silent-catch pattern throughout storage.ts and the Promise.all silent catch in clearAllAuthData.
Multiple auth/signup endpoints return account-existence and account-state signals for arbitrary inputs: requestOtp returns the numeric user_id (confirming registration), signup Step 1 returns a deleted_account message revealing a prior deleted account, and dedicated validate/email and validate/phone endpoints return existence verdicts. The numeric IDs are sequential and client-exposed, compounding enumeration.
requestOtp / validate-email across a list of emails; positive responses (with a user_id, or a deleted-account message) confirm which belong to A-List, building a target list for AA-07/AA-03.Privacy disclosure and reconnaissance: an attacker can enumerate registered users, identify deleted accounts, and harvest valid targets for phishing, credential-stuffing, and OTP brute-force (AA-07), feeding the other auth weaknesses.
user_id, deleted-account state, or differential messages. Rate-limit and add anti-automation to validate/email, validate/phone, and OTP-request. Use opaque non-sequential identifiers.Client-side evidence (id returned, deleted_account message, validate endpoints) confirmed. 'likely' as exploitability depends on the backend's response differentiation, which is strongly implied by the client handling.
On the web target the YouTube OAuth authorization-code flow is built without PKCE, and the state parameter is base64(JSON{creator_id}) — guessable and providing no CSRF protection. The popup result is received via window.addEventListener('message', handleMessage) which reads event.data.code without validating event.origin, so any window/iframe can post a forged message containing an attacker-chosen authorization code, which the app forwards to connectYoutube.
{type:'youtube-oauth', code:<attacker code>} to the window; the unvalidated handler forwards it to /connect-youtube, linking the attacker's channel to the victim, or hijacking the linkage via the forgeable state.On web, authorization-code injection / OAuth CSRF can cause a victim to link an attacker-controlled YouTube channel (or vice versa). Limited blast radius (channel-connect, not primary auth), hence P3.
code_challenge) for the web authorization-code flow, use a cryptographically random unguessable state stored and verified on return, and validate event.origin against the expected Google/Expo auth origin in the postMessage handler before consuming the code.Confirmed no PKCE, base64(creator_id) state, and absent origin check by reading YouTubeConnectButton.tsx. Merges the duplicate mobile_api_auth P3 (PKCE/origin) and mobile_transport P3 (state nonce) findings.
The app copies Instagram/TikTok verification PINs and redeemable voucher codes to the global system clipboard via expo-clipboard. On Android (pre-12) and iOS, clipboard contents are readable by any installed app, and iOS syncs the clipboard across devices via Universal Clipboard. Values are not cleared after use and no sensitivity flag is set.
A malicious or background app on the same device can read social-verification PINs (enabling verification hijack) or single-use voucher codes (redemption fraud). Requires a co-resident hostile app; low value per item.
ClipDescription EXTRA_IS_SENSITIVE on Android 13+) and clear the clipboard after a short timeout. Re-evaluate whether verification PINs/voucher codes need clipboard copy or can be auto-filled/deep-linked.Confirmed all six call sites by grep and reading Step3Verification.tsx.
generateTestFCMToken() builds a 152-char device token from Math.random(), a non-cryptographic PRNG. Although labelled 'test', it is not dead code — tokenRefresh.ts (AA-02) and the login flow (formHandler.ts:421) call it to generate the device_token persisted via saveDeviceToken and registered against /user/fcm_token, which is the same endpoint used to re-mint the JWT.
Math.random() sequence could predict or collide device tokens used in registration, aiding push/device-identity spoofing in conjunction with a captured JWT.The device token is low-entropy and predictable. As-is it gates push registration and the FCM-based token re-mint; predictable device identifiers could aid spoofing of device registration. Direct authentication impact is limited because the JWT (not the device token) authorizes the call, hence P3.
Math.random() with a CSPRNG: expo-crypto getRandomBytes/getRandomValues. Better, source the real FCM/Expo push token from expo-notifications rather than minting a synthetic one. Never use Math.random() for tokens, nonces, or IDs.Confirmed implementation and that it is reachable from production paths (tokenRefresh.ts and formHandler.ts call it), correcting the scanner's 'dead/test-only' characterization; security impact remains low (P3) because the JWT, not this value, is the credential.
When a push notification is tapped, useNotifications.ts interpolates the server-supplied data.offerId directly into an Expo Router path string without validating it is a numeric id. A crafted push payload could embed path segments or extra query parameters (an offerId containing / or ?/&) to redirect navigation to an unintended internal route or inject route params. All reachable targets are internal Expo Router screens (no external URL, no WebView, no native intent), bounding impact to in-app navigation/parameter confusion.
'1/../profile?foo=bar'; tapping it navigates the user to an unintended internal screen with attacker-chosen params.Low: an actor controlling push payloads can steer a tapped notification to an unintended in-app screen or smuggle route params (UI redress/confusion), with no direct data or privilege escalation since downstream screens still enforce server-side authorization.
const id = Number(data.offerId); if (!Number.isInteger(id) || id <= 0) return;) and use the structured params form router.push({ pathname: '/offer/[id]', params: { id: String(id), origin: 'notifications' } }), mirroring notificationNavigation.ts.Confirmed the unvalidated interpolation in useNotifications.ts and the safer sibling in notificationNavigation.ts. 'likely' because exploitability depends on Expo Router's path parsing tolerance for injected segments.
EXPO_PUBLIC_INTERCOM_TOKEN from eas.json and config/environment.ts; purge it from git history (git-filter-repo/BFG) and force-push with team coordination.AIzaSy... key has GCP application + API restrictions; stop tracking google-services.json; add gitleaks pre-commit/CI with a tok: rule.production EAS profile to target the real prod API (https://api.alist.ae/v1) — this alone neutralizes the AA-10 OTP-leak dev-gate and the AA-06 dev-backend WebView trust issue./user/fcm_token as a refresh oracle; add a dedicated refresh-token endpoint with rotation and a server-side /auth/logout revocation call wired into both logout() and handleUnauthorizedSession().user_id from verify-otp; resolve identity server-side from email+OTP only; switch to opaque non-sequential IDs.javaScriptEnabled={false}, originWhitelist=['https://*.alist.ae'], onShouldStartLoadWithRequest host gating, setSupportMultipleWindows={false}.console.* and OTP-in-toast statements; gate debug on __DEV__.npm ci.npm ci + npm audit --audit-level=high/snyk test + lint + typecheck; add Dependabot/Renovate and per-release SBOM scanning.*.alist.ae via expo-build-properties + network_security_config / TrustKit, with a backup pin and CT.buildUrl reject non-https schemes + enforce a host allowlist; add Android cleartext=false and iOS ATS; validate EXPO_PUBLIC_API_BASE_URL at startup.react-native-render-html or sanitize/allowlist anchors and server-side-sanitize HTML.expo-screen-capture / FLAG_SECURE on OTP, verification-code, and voucher screens; iOS background blur.state, and event.origin validation in the web YouTube OAuth flow.Math.random() device-token generation with expo-crypto / the real Expo push token.offerId to a positive integer and use structured router params before navigating from push payloads.any annotations dilute the benefit. tsc --noEmit is not part of any pipeline.no-console, no prefer-const, no import sorting. npm run lint exists but never runs on CI (there is no CI).features/offers/api/offer.ts (~600 LOC, 8 raw fetch calls), shared/utils/storage.ts (~420 LOC, all errors swallowed), and the multi-step signup flow in features/auth/ are the densest files; touch with care.StoreProvider. queries/index.ts is a near-empty re-export. Two HomeConstants.ts and ProfileConstants.ts. utils/storage.ts is a single-line re-export kept for legacy reasons.react-native-swiper and react-native-render-html are the least-maintained — the latter is archived and is flagged as AA-15.fetch vs request.ts, two store providers, two constants directories. Looks like an in-progress refactor that was paused.app/index.tsx uses accessibilityLabel / accessibilityRole and respects AccessibilityInfo.isReduceMotionEnabled(). Not audited beyond the entry screen.i18n-iso-countries is only used for country list lookups.# Node 20+ recommended (Expo SDK 54)
npm install
# Optional: copy secrets template for Google YouTube client IDs
cp config/secrets.example.ts config/secrets.ts
# fill in real values
# Start Expo dev server
npm run start
# Or platform-specific
npm run ios # expo run:ios (needs Xcode + CocoaPods)
npm run android # expo run:android (needs Android SDK + JDK 17)
npm run web
# Lint
npm run lint
EXPO_PUBLIC_ to be exposed to the JS bundle. Note: EXPO_PUBLIC_ values are statically inlined into the shipped bundle — never put a server-side secret behind this prefix (see AA-01).EXPO_PUBLIC_API_BASE_URL (default fallback: https://dev-api.alist.ae/v1)EXPO_PUBLIC_VENUE_IMAGE_BASE_URLEXPO_PUBLIC_GOOGLE_YOUTUBE_CLIENT_ID + _ANDROID_CLIENT_ID + _IOS_CLIENT_IDEXPO_PUBLIC_INTERCOM_APP_ID, _ANDROID_API_KEY, _IOS_API_KEY, _TOKEN — the _TOKEN value is a privileged Intercom REST Access Token and must be removed (AA-01).google-services.json is checked in at the repo root and referenced from app.json. The Firebase web API key inside it is bundle-visible — normal for Firebase but must be GCP-restricted (AA-14).ae.alist.android / iOS ae.alist.ios. URL scheme: alistapplication.Builds run through EAS Build (project id 05f9a62a-4bc2-4bb9-bd51-c78b52e52ad8). Six profiles exist in eas.json: development, preview, preview2, preview3, preview4, production — all six currently set the API URL to the dev environment (a systemic misconfiguration that amplifies AA-06 and AA-10). No submit profiles are defined. There is no bitbucket-pipelines.yml or GitHub Actions workflow in the repo — EAS builds are kicked off manually with eas build --profile <name> --platform <ios|android>. There is no SCA/dependency scanning, lockfile, or crash reporting (see AA-05, AA-08).
ae.alist.ios) lives here too. Don't assume Android-only when troubleshooting.eas.json. The dev URL leaks into production, and so does the privileged Intercom token. Same goes for any new env var you add.shared/utils/request.ts helpers for new endpoints; do not copy the raw-fetch pattern from features/…/api/…. Note request.ts currently accepts cleartext URLs and is unpinned (AA-04/AA-12).saveProfileData in shared/utils/storage.ts — it drops fields without logging (AA-16).core/providers/StoreProvider.tsx; the features/offers/store/offersStore.tsx twin is a duplicate but still imported in app/_layout.tsx (line 4).shared/utils/tokenRefresh.ts — and note this is the AA-02 indefinite-re-mint weakness.refetchOnMount: false). When debugging "stale" UI, manually invalidate or pull-to-refresh rather than restart.AnimatedSplashScreen finishes; if you touch app/_layout.tsx's splash gating you can ship a build with a permanent white screen.app/_layout.tsx:90). Expected, but spams Intercom in development.scripts/, docs/, the lockfile, and android/ are gitignored. This is a managed Expo workflow. Don't commit native folders — but DO re-commit the lockfile (AA-05).shared/utils/storage.ts (SecureStore on native, localStorage on web).