← All repos

alist-android

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.

Primary stackReact Native 0.81 / Expo SDK 54 / TypeScript Last commit2026-05-14 Repo size103 MB Files359 Branchmain Health Critical

Executive summary

Tech stack

Expo SDK 54 React Native 0.81.4 React 19.1 TypeScript 5.9 (strict) Expo Router 6 Zustand 5 TanStack Query 5 Reanimated 4 Intercom RN 9 New Architecture EAS Build
CategoryTechnologyVersionNotes
RuntimeReact Native0.81.4newArchEnabled: true in app.json
FrameworkExpo^54.0.7Current. Includes expo-dev-client, splash, font, secure-store, notifications, video plugins
RoutingExpo Router~6.0.7File-based. experiments.typedRoutes enabled. Tabs + Stack groups
LanguageTypeScript~5.9.2Strict on. ~120 explicit any annotations remain
State (client)Zustand^5.0.8authStore, uiStore in core/state/
State (server)@tanstack/react-query^5.90.2staleTime 30 min, gcTime 60 min, refetchOnMount: false
Persistenceexpo-secure-store~15.0.7Tokens + form/profile data. Web fallback: localStorage
Pushexpo-notifications + custom FCM^0.32.11Token refresh repurposes FCM register endpoint to mint fresh JWT
Support chat@intercom/intercom-react-native^9.1.1Tab press intercepted in app/main/_layout.tsx
Auth providers@react-native-google-signin/google-signin^16.1.2Google client IDs in app.json & eas.json
Linteslint-config-expo (flat)~10.0.0Default rules only. No custom rules.
TestsNone. No Jest, RNTL, Detox, or Maestro.
CINo bitbucket-pipelines.yml. Builds via EAS CLI.
Crash reportingNo Sentry / Crashlytics / Bugsnag.

Architecture

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).

alist-android/ ├── app/ # Expo Router routes │ ├── _layout.tsx # Root: SafeArea, GestureHandler, KeyboardProvider, ReactQuery, BottomSheet, Toast, Intercom setup │ ├── index.tsx # Splash video + auth gate (redirects to /main/home) │ ├── auth/ # login.tsx, signUp.tsx, Congratulations.tsx │ ├── main/ # Tabs: chat (Intercom), history, home, profile, notifications │ ├── offer/[id].tsx # Modal offer detail (presentation: "modal") │ └── profile/ # Stack: personal-details, social-profiles, niches, otp ├── core/ │ ├── providers/ # ReactQueryProvider, StoreProvider (mostly pass-through) │ └── state/ # authStore.ts (Zustand), uiStore.ts ├── features/ # 8 feature slices: auth, offers, profile, home, history, notifications, venues, common │ └── <name>/ # api/, components/, hooks/, queries/, (+ utils/ styles/ providers/) ├── shared/ │ ├── api/ # countries, locations, categories, legal │ ├── components/ui/ # Button, Input, Dropdown, PhoneField, … │ ├── hooks/ # useBottomSheet, useDebounce, useValidation, … │ └── utils/ # request.ts (auth wrapper), storage.ts (SecureStore), tokenRefresh.ts, session.ts, intercom.ts ├── config/ │ ├── environment.ts # EXPO_PUBLIC_* lookup with hardcoded fallbacks │ └── secrets.example.ts # Template (real secrets.ts is gitignored) ├── constants/ # Colors, sizes, copy ├── types/ # Domain TS types ├── assets/ # ~50MB: fonts, icons, images, 38MB intro mp4 ├── app.json # Expo config: scheme, bundle ids, plugins, intent filters ├── eas.json # 6 build profiles (dev → production) └── google-services.json # Firebase config (committed)

Security assessment — methodology

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.

OWASP Top 10 2021 OWASP WSTG OWASP API Top 10 2023 OWASP MASVS (mobile only) CWE CVSS 3.1 NIST CSF 2.0
Anthropic cybersecurity-skills playbooks applied to this repo (mobile + supply-chain dimensions):
mobile-storage · mobile-api-auth · mobile-transport · mobile-platform · supply-chain-frontend

Risk narrative

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.

Overall risk
Critical
1P0 5P1 6P2 6P3

Systemic themes

OWASP coverage

CategoryStatusNotes
M1: Improper Credential UsageVulnAA-01 privileged Intercom Access Token committed and bundled; AA-14 other keys committed in config
M2: Inadequate Supply Chain SecurityVulnAA-05 no lockfile, AA-08 no CI/SCA, AA-15 unmaintained render-html, AA-14 keys-in-repo
M3: Insecure Authentication/AuthorizationVulnAA-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 ValidationVulnAA-06 WebView no origin gating, AA-15 unsanitized HTML, AA-21 unvalidated push offerId
M5: Insecure CommunicationVulnAA-04 no TLS pinning, AA-12 cleartext http accepted + token attached, no ATS/cleartext config
M6: Inadequate Privacy ControlsVulnAA-11 PII/OAuth code logged to console; AA-01 enables bulk PII export; AA-17 user enumeration
M7: Insufficient Binary ProtectionsPartialManaged Expo build; no obfuscation/anti-tamper, but no native code reviewed — EXPO_PUBLIC_ secrets trivially extractable from bundle (AA-01)
M8: Security MisconfigurationVulnAll 6 EAS profiles point at dev-api in production; WebView insecure defaults (AA-06); no cleartext/ATS hardening (AA-12)
M9: Insecure Data StorageVulnAA-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 CryptographyVulnAA-20 Math.random() for device tokens; AA-18 guessable OAuth state nonce
API1:2023 Broken Object Level AuthorizationVulnAA-03 OTP verify trusts client-supplied user_id
API2:2023 Broken AuthenticationVulnAA-02, AA-07, AA-09, AA-10, AA-17 — token lifecycle and OTP weaknesses across the auth surface
A02:2021 Cryptographic FailuresVulnAA-12 token eligible to traverse cleartext; AA-04 no pinning
A05:2021 Security MisconfigurationVulnAA-01/AA-14 secrets committed; dev API in prod build profiles
A06:2021 Vulnerable and Outdated ComponentsVulnAA-05 no lockfile, AA-08 no SCA, AA-15 archived render-html, version-skewed/dead deps
A09:2021 Security Logging & Monitoring FailuresVulnAA-11 sensitive console logging in prod; AA-16 silent error swallowing; no crash reporting/telemetry

Detailed findings

P0 critical · 1 P1 high · 7 P2 medium · 7 P3 low / info · 6

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.

AA-01

Intercom server-side Access Token committed to git and bundled into every build

P0 M1: Improper Credential Usage / A05:2021 CWE-798 CVSS 8.6 confirmed
Description

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.

Evidence
eas.json:16,30,44,56,74,88 — all 6 build profiles set EXPO_PUBLIC_INTERCOM_TOKEN: "dG9rOjQwZTQzN2NiXzBhZDVfNGQ3Nl85YTA0XzRlZmRlYzVjNjJkNzoxOjA=" Verified base64 decode: `echo dG9r...| base64 -d` => `tok:40e437cb_0ad5_4d76_9a04_4efdec5c62d7:1:0` — the `tok:` prefix is Intercom's Access Token (server-side REST API credential) format, NOT a public android_sdk-/ios_sdk- SDK key config/environment.ts:56 token: getEnvVar("INTERCOM_TOKEN", "") reads EXPO_PUBLIC_INTERCOM_TOKEN into Environment.intercom.token git log -- eas.json: committed in commit e84bb6d 'json configs and gitignore updates'; still present at HEAD EXPO_PUBLIC_ prefix => Expo statically inlines the value into the JS bundle at build time, so it ships inside every APK/IPA/web build and is recoverable by unzipping the bundle, in addition to anyone with repo read access
Attack scenario
An attacker downloads the published APK/IPA (or clones the repo / obtains a leaked mirror), unzips the JS bundle, greps for the base64 string, decodes it to the 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.
Impact

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.

Remediation
Treat as a live credential leak. (1) Immediately rotate/revoke the Intercom Access Token in the Intercom dashboard. (2) Remove 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.
Verifier note

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.

AA-02

JWT indefinitely re-minted via FCM device-registration endpoint; defeats session expiry and revocation

P1 API2:2023 Broken Authentication / M3:2024 CWE-613 CVSS 8.1 confirmed
Description

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.

Evidence
shared/utils/tokenRefresh.ts:36-56 refreshAuthToken(): POSTs to the FCM registration endpoint via registerFCMToken(); if fcmResponse.data?.token is present it treats it as a fresh access token and saveToken(freshToken) shared/utils/tokenRefresh.ts:13 comment: 'This leverages the fact that FCM API returns a fresh JWT token' features/notifications/api/fcm.ts:17-57 registerFCMToken POSTs to ${baseUrl}/user/fcm_token authenticated only by the current Bearer token and returns data.token shared/utils/request.ts:79-99 handle401AndMaybeRetry(): on 401 with retryOn401 (default true for get/post/put/patch/del at lines 149,166,184,203,219) calls refreshAuthToken() and retries shared/utils/tokenRefresh.ts:76-83 shouldRefreshToken() hardcoded to return false; comment 'in production you'd decode the JWT and check exp claim' — client never validates exp
Attack scenario
An attacker who captures a creator's JWT (via MITM on the unpinned channel AA-06, device-log leakage, or a backup) calls 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).
Impact

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.

Remediation
Implement a dedicated refresh-token endpoint with rotation and server-side revocation; stop overloading /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.
Verifier note

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'.

AA-03

OTP verification trusts client-supplied user_id, enabling account-takeover via parameter manipulation

P1 API1:2023 Broken Object Level Authorization / M3:2024 CWE-639 CVSS 8.1 likely
Description

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.

Evidence
features/auth/api/auth.ts:50-64 verifyOtp(email, otp, userId) builds body = { email, otp, user_id: userId } and POSTs to ${BASE_URL}/login/verify-otpv2 shared/utils/formHandler.ts:361 setUserId(data.data?.id || data.user_id) — userId is taken verbatim from the requestOtp response and replayed shared/utils/formHandler.ts:395 verifyOtp(email, Number(otp), userId); then on success finalUserId = data.data?.user_id || userId || data.data?.id (line 416) and authStore.login(finalToken, finalUserId, finalEmail) features/auth/api/auth.ts:5-48 requestOtp(email) returns data.data.id (the numeric account user_id), exposing a sequential identifier to the client/attacker
Attack scenario
Attacker calls requestOtp for their own email, receives a valid OTP they control, then POSTs /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.
Impact

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.

Remediation
Remove 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.
Verifier note

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.

AA-04

No certificate or public-key pinning on any TLS connection (full MITM exposure of JWTs, OTPs, PII)

P1 M5: Insecure Communication / MASVS-NETWORK-1 CWE-295 CVSS 6.8 confirmed
Description

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.

Evidence
Repo-wide grep for sslPinning|TrustManager|publicKeyHash|react-native-ssl-pinning|certificate-transparency|trustkit|expo-build-properties returned ZERO hits (verified across .ts/.tsx/.json, excluding node_modules/.git) package.json dependencies contain no pinning library and no expo-build-properties; app.json plugins[] has no androidNetworkSecurityConfig shared/utils/request.ts:46-59 execute(): plain await fetch(finalUrl, ...) with no TLS validation hook — the only transport control is the OS trust store shared/utils/request.ts:36-37 attaches Authorization: Bearer <token> to every request on this un-pinned channel features/auth/api/auth.ts:10,58 requestOtp/verifyOtp POST email+OTP over the same un-pinned fetch; no committed android/ or ios/ native dirs to add a network_security_config.xml/Info.plist
Attack scenario
Victim joins an attacker-controlled Wi-Fi network whose captive portal installs (or has previously installed) a user CA; the attacker terminates TLS transparently, reads the Bearer token and the in-flight OTP, then replays the token and uses AA-02 to keep it alive indefinitely.
Impact

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.

Remediation
Add SPKI public-key pinning for *.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.
Verifier note

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).

AA-05

Dependency lockfile deleted and gitignored; floating version ranges with no integrity pinning

P1 A06:2021 Vulnerable & Outdated Components / M2:2024 CWE-1357 CVSS 7.4 confirmed
Description

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.

Evidence
git log --all -- package-lock.json: added at first push b58bfcc, then deleted in commit e8b1336 'Add/update app/main/history.tsx' .gitignore:96-97: 'package-lock-copy.json' and 'package-lock.json' explicitly excluded No package-lock.json / yarn.lock / pnpm-lock.yaml / npm-shrinkwrap.json present in the working tree (ls confirms absence) package.json declares ~60 floating ranges (caret/tilde), e.g. expo:^54.0.7, react-native-render-html:^6.3.4, @intercom/intercom-react-native:^9.1.1, zustand:^5.0.8 README.md / package.json document only 'npm install' (no 'npm ci'); there is no lockfile for npm ci to consume
Attack scenario
A maintainer of a deep transitive dependency (or a typosquat that gets pulled) publishes a malicious patch release within a caret range; the next EAS production build silently resolves it, and a postinstall script or runtime payload ships to all users with no review gate to catch it.
Impact

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.

Remediation
Commit a fresh 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.
Verifier note

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.

AA-06

WebView (InAppBrowser) loads server-controlled URL with insecure defaults: JS enabled, no origin allowlist, no navigation gating

P1 M4: Insufficient Input/Output Validation / MASVS-PLATFORM-2 CWE-749 CVSS 6.1 confirmed
Description

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.

Evidence
shared/components/ui/InAppBrowser.tsx:88-98 <WebView source={{ uri: url }} startInLoadingState androidLayerType="software" /> — no javaScriptEnabled={false}, no originWhitelist, no onShouldStartLoadWithRequest, no setSupportMultipleWindows={false}, no allowFileAccess={false}. react-native-webview defaults JavaScript ON shared/api/legal.ts:5-25 and features/home/api/legal.ts:5-25 fetchUserTerms()/fetchPrivacyPolicy() do UNAUTHENTICATED GET ${baseUrl}/user-terms and /privacy-policy and return the URL/HTML rendered in the WebView app/main/profile.tsx and features/auth/components/Step3.tsx feed the API-returned terms/privacy URL straight into <InAppBrowser url=...>; value is also cached in SecureStore (storage.ts LEGAL_URLS_KEY) All six EAS profiles point at https://dev-api.alist.ae/v1, so the production app fetches this WebView URL from the lower-trust dev backend over an unauthenticated endpoint No injectedJavaScript / JS bridge is exposed on the WebView (limits direct native RCE)
Attack scenario
On an unpinned/MITM'd connection (or via the dev backend), the attacker substitutes the /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.
Impact

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.

Remediation
Set 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.
Verifier note

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.

AA-07

4-digit login OTP with no client-side throttling, lockout, or resend cooldown (brute-force exposure)

P1 API2:2023 Broken Authentication / M3:2024 CWE-307 CVSS 7.5 likely
Description

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.

Evidence
features/auth/utils/loginHelpers.ts LOGIN_CONSTANTS OTP_LENGTH: 4 — login OTP is a 4-digit numeric code (10,000 keyspace) shared/utils/formHandler.ts:383-458 handleLoginContinue verify branch: verifyOtp called directly on each submit with no attempt counter, no lockout, no client backoff features/auth/utils/loginHelpers.ts:162-201 handleResendOtp: no client-side cooldown enforced features/auth/api/auth.ts:50-96 verifyOtp performs a single POST with no anti-automation token (no captcha/nonce)
Attack scenario
Attacker triggers requestOtp for a victim email, then scripts POST /login/verify-otpv2 iterating 0000-9999; with no client/anti-automation friction the only barrier is unverified server throttling.
Impact

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.

Remediation
Increase OTP length to at least 6 digits and enforce strict server-side controls: 3-5 attempts per OTP, short TTL (2-5 min), single-use invalidation, per-account and per-IP rate limiting, resend cooldown, and anti-automation (nonce/captcha) on repeated failures. Mirror sane resend cooldowns in the client UI.
Verifier note

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.

AA-08

No CI/CD pipeline and no software-composition/dependency scanning — zero automated supply-chain controls

P1 A06:2021 Vulnerable & Outdated Components / M2:2024 CWE-1104 CVSS 6.5 confirmed
Description

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.

Evidence
No .github/, .circleci/, .gitlab-ci.yml, or bitbucket-pipelines.yml anywhere in the tree (ls/find confirm absence) No .snyk, renovate.json, .renovaterc, or .github/dependabot.yml present package.json scripts are only: start, reset-project, android, ios, web, lint — no test, no audit, no npm ci step eslint.config.js loads only eslint-config-expo defaults — no security/SCA linting; builds are manual via EAS CLI
Attack scenario
A high-severity CVE is disclosed in a transitive dependency; with no SCA or dependency-bot, it remains in the app across releases until a human happens to notice, while a malicious version (AA-05) would ship with equally no detection.
Impact

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.

Remediation
Stand up a CI pipeline (Bitbucket Pipelines/GitHub Actions). Minimum jobs: 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.
Verifier note

Confirmed absence of all CI/SCA config. Distinct from AA-05 (integrity anchoring) — this is the missing detection/gate layer; both are systemic.

AA-09

Logout and force-logout do not revoke the token server-side (client-only session teardown)

P2 API2:2023 Broken Authentication CWE-613 CVSS 5.4 confirmed
Description

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.

Evidence
core/state/authStore.ts:86-150 logout(): clears UI state + React Query cache, unregisterFCMToken (DELETE /user/fcm_token), clearAllAuthData() local deletes, intercomLogout — but never calls a backend /logout or token-invalidation endpoint shared/utils/storage.ts:404-418 clearAllAuthData() only deletes local SecureStore keys shared/utils/session.ts:11-33 handleUnauthorizedSession() (401 force-logout) only router.replace + authStore.logout() — purely local No /auth/logout or token-revocation endpoint exists anywhere in features/*/api (grep)
Attack scenario
An attacker who copied a JWT (shared device, prior MITM, log capture) keeps using it after the legitimate user taps 'log out'; with AA-02 the attacker also re-mints it, so logout provides no protection.
Impact

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.

Remediation
Add a backend session-revocation call (e.g. 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.
Verifier note

Confirmed by reading authStore.logout, clearAllAuthData, session.ts, and grepping for a logout endpoint (none). Compounds AA-02.

AA-10

OTP value returned in API response and shown in a Toast whose dev-gate is true in production

P2 API2:2023 Broken Authentication / M9:2024 CWE-532 CVSS 5.3 confirmed
Description

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.

Evidence
shared/utils/formHandler.ts:352-360 otpValue = data.data?.otp || data.otp; isDevelopment = Environment.api.baseUrl.includes("dev-"); if (isDevelopment) Toast.show({ text1: `OTP sent successfully ${otpValue}` }) shared/utils/formHandler.ts:148-156 same OTP-in-toast pattern in the signup flow features/auth/utils/loginHelpers.ts:172-187 handleResendOtp shows the OTP in a Toast when Environment.isDev eas.json: ALL profiles incl. production set EXPO_PUBLIC_API_BASE_URL=https://dev-api.alist.ae/v1, so baseUrl.includes('dev-') evaluates TRUE in production builds The reads data.data?.otp || data.otp imply the backend returns the OTP value in the /login/with-otp response body
Attack scenario
On a MITM'd connection the attacker reads the /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.
Impact

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.

Remediation
Never return the OTP in the API response — deliver it only out-of-band (email). Remove all OTP-in-toast statements. Gate any debug behaviour on a true build-time flag (__DEV__), not a runtime substring match on the API host. Fix the production EAS profile to target the prod API.
Verifier note

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.

AA-11

Full profile/signup API responses and OAuth code/state logged to console in production (no __DEV__ guard)

P2 M9: Insecure Data Storage / Logging / A09:2021 CWE-532 CVSS 4.0 confirmed
Description

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.

Evidence
features/profile/api/profile.ts:9 console.log("DEBUG: fetchUserProfile response:", JSON.stringify(data, null, 2)) and :16 fetchSocialMediaProfile — dump full profile PII (name, email, social handles) features/auth/api/signup.ts:299 console.log("Request Data:", JSON.stringify(data, null, 2)) and :316 Response Body — connectYoutube payload includes OAuth code, state, redirect_uri features/auth/api/auth.ts:8 console.log('LOGIN API URL:', `${BASE_URL}/login/with-otp`); auth.ts:34,82 console.error first 200 chars of non-JSON server responses None of these console.* calls are guarded by __DEV__; they execute in release builds (no babel-plugin-transform-remove-console configured)
Attack scenario
During a vendor support session (or via a co-resident app with log access on an older device), an actor runs adb logcat and captures the dumped profile JSON and the YouTube OAuth code/state, then replays the code against /connect-youtube.
Impact

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.

Remediation
Remove all DEBUG/response-body 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.
Verifier note

Confirmed all cited lines. Merges the mobile_storage P2 PII-logging and mobile_transport P3 auth-URL-logging findings (same root cause).

AA-12

request layer accepts cleartext http:// URLs and attaches the Bearer token to any caller-supplied absolute URL

P2 M5: Insecure Communication / A02:2021 CWE-319 CVSS 5.4 confirmed
Description

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.

Evidence
shared/utils/request.ts:18-27 buildUrl(): `if (url.startsWith("http://") || url.startsWith("https://")) return url;` — cleartext http:// is accepted and used verbatim, no scheme rejection, no host allowlist shared/utils/request.ts:29-44 withAuthHeaders attaches Authorization: Bearer <token> to whatever finalUrl buildUrl returns, including http:// or third-party https hosts app.json declares no Android usesCleartextTraffic:false / networkSecurityConfig and no iOS ATS hardening (grep for usesCleartextTraffic|NSAllowsArbitraryLoads|networkSecurityConfig returned nothing) constants/HomeConstants.ts:6 (and features/offers/constants/HomeConstants.ts:6) IMAGE_BASE_URL derives a host by string-mangling baseUrl (.replace('/v1','').replace('api','admin')) with no scheme guard
Attack scenario
A server-returned or misconfigured URL with an 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.
Impact

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.

Remediation
In 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.
Verifier note

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).

AA-13

No screen-capture protection on screens showing OTPs, social-verification codes, and voucher codes

P2 M9: Insecure Data Storage / MASVS-STORAGE-2 CWE-200 CVSS 4.0 confirmed
Description

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.

Evidence
Repo-wide grep for FLAG_SECURE|expo-screen-capture|usePreventScreenCapture|preventScreenCapture returned ZERO hits; expo-screen-capture is not in package.json features/profile/components/VerifyAccountBottomSheet.tsx renders the social verificationCode on screen features/auth/* render OTP entry; features/home/components/OfferVoucher.tsx:119,197 and features/offers/components/OfferVoucher.tsx:125,244 display/copy voucherCode app.json android block sets no windowSecureFlags
Attack scenario
Screen-recording malware (or the OS Recents thumbnail cache) captures the OTP/verification-code screen; the attacker later reads the code from the cached image and completes verification or redeems the voucher.
Impact

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.

Remediation
Add 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.
Verifier note

Confirmed absence by grep and dependency review; sensitive-render sites confirmed.

AA-14

Intercom SDK keys, Firebase API key, OAuth client IDs and signing-cert hash committed in config (rotation/hygiene risk)

P2 A05:2021 Security Misconfiguration / M2:2024 CWE-538 CVSS 4.3 confirmed
Description

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.

Evidence
app.json:90-91 Intercom androidApiKey android_sdk-b7f1f628..., iosApiKey ios_sdk-ed1d62a8...; config/environment.ts:48-55 same SDK keys as hardcoded fallbacks; eas.json repeats them in all 6 profiles google-services.json:31 (git-tracked, not gitignored — committed in e84bb6d) current_key Firebase/Google API key AIzaSyAN4IeHV0WYoMrZmj0zqLHWWixvbHDdn6c and :21 Android certificate_hash 347de11484b5520447d46e5cff249eab092e5a99 app.json:81-83 Google OAuth iosClientId/webClientId and :42 reverse-client-id intent-filter scheme .gitignore lists google-services-copy.json but NOT google-services.json — the real file is tracked
Attack scenario
An actor with repo/clone access extracts the Firebase API key and, if GCP application/API restrictions are absent, drives billed requests against enabled Google APIs or probes which services are enabled for the project.
Impact

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.

Remediation
Verify in GCP Console that the 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.
Verifier note

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).

AA-15

Deprecated/unmaintained react-native-render-html renders server-supplied HTML through an archived parser tree

P2 A06:2021 Vulnerable & Outdated Components / M2:2024 CWE-1104 CVSS 5.3 confirmed
Description

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.

Evidence
package.json:65 react-native-render-html ^6.3.4 — the package and its @native-html/* org are archived/unmaintained; v6 receives no security updates Reachable across 8 component files (verified by grep): features/home/components/{OfferContent,Deliverables,Upload,OfferVoucher}.tsx and features/offers/components/{OfferContent,Deliverables,Upload,OfferVoucher}.tsx features/home/components/OfferContent.tsx:33,153-156 source={{ html: description }} where description = apiData?.data?.voucher?.description — server-supplied, attacker-influenceable content Components set tagsStyles but no renderersProps anchor onPress / scheme allowlist (OfferContent.tsx:157, Deliverables.tsx:139,208,270) and no sanitizer Pulls in deprecated transitive HTML/CSS parsing stack (htmlparser2 / domutils / css-select / nth-check era) that no longer receives upstream fixes; also corroborates AA-05 (no lockfile to pin transitive versions)
Attack scenario
Backend (or merchant-submitted) content for a voucher description includes a styled anchor to a phishing https page or a custom-scheme deep link; the user taps it inside the trusted app and is redirected, or a crafted pathological HTML/CSS string drives the unmaintained parser into a ReDoS stall.
Impact

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.

Remediation
Replace 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.
Verifier note

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.

AA-16

Storage layer swallows all errors silently across token/PII persistence (hides failed logout/clears and tampering)

P3 M9: Insecure Data Storage / A09:2021 CWE-390 CVSS 2.2 confirmed
Description

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.

Evidence
shared/utils/storage.ts:23-25,52-54,67-69,98-100 etc. — every save/clear catch block is `catch (error) { // Silently handle error }` shared/utils/storage.ts:40-42,84-86 getToken/getFormData return null on any error, masking SecureStore decryption/tamper failures shared/utils/storage.ts:404-418 clearAllAuthData wraps Promise.all in a silent catch — a failed clear during logout leaves tokens/PII on device with no signal No crash reporting wired up (CLAUDE.md confirms no Sentry/Crashlytics), so these failures are invisible in production
Attack scenario
Logout's 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.
Impact

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).

Remediation
Stop silently swallowing storage errors. Route them to a redacting logger / crash reporter (add Sentry/Crashlytics). For logout, verify each clear succeeded and surface/force-retry on failure rather than assuming success. Log only the operation + error class, never the values.
Verifier note

Confirmed the silent-catch pattern throughout storage.ts and the Promise.all silent catch in clearAllAuthData.

AA-17

Account-status enumeration via differential responses and exposed sequential user IDs

P3 API2:2023 Broken Authentication CWE-204 CVSS 5.3 likely
Description

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.

Evidence
shared/utils/formHandler.ts:104-141 submitStep1 surfaces a backend message disclosing deleted_account === true ('We've noticed a prior association with your profile...'), revealing the email previously had an account features/auth/api/auth.ts:5-48 requestOtp returns data.data.id (numeric account user_id) for a given email — confirms existence and leaks the id shared/utils/formHandler.ts:361 setUserId(data.data?.id || data.user_id) — sequential numeric ids handed to client features/auth/api/signup.ts:166,259 validateEmail / validatePhone endpoints return existence verdicts for arbitrary email/phone
Attack scenario
Attacker scripts 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.
Impact

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.

Remediation
Return uniform, non-committal responses for existence checks; do not reveal 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.
Verifier note

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.

AA-18

Web YouTube OAuth lacks PKCE, uses a guessable base64(creator_id) state, and the postMessage handler has no origin check

P3 M3:2024 Insecure Authentication/Authorization CWE-346 CVSS 4.3 confirmed
Description

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.

Evidence
features/auth/components/YouTubeConnectButton.tsx:82-90 web authUrl built with response_type=code but NO code_challenge/code_challenge_method (no PKCE) YouTubeConnectButton.tsx:103-125 handleMessage reads event.data.code and forwards it to connectYoutube with NO event.origin validation YouTubeConnectButton.tsx:74 const state = btoa(JSON.stringify({ creator_id: creatorId })) — predictable, no random anti-CSRF nonce (same pattern in features/profile/components/YouTubeProfileItem.tsx:74) Native flow (lines 133-157) uses GoogleSignin (less exposed); the web flow is the interceptable path
Attack scenario
A malicious page opened alongside the app posts {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.
Impact

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.

Remediation
Enforce PKCE (S256 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.
Verifier note

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.

AA-19

Sensitive verification PINs and voucher codes copied to the global system clipboard with no auto-expiry or sensitivity flag

P3 M9: Insecure Data Storage — clipboard exposure CWE-200 CVSS 2.4 confirmed
Description

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.

Evidence
features/auth/components/Step3Verification.tsx:26 await Clipboard.setStringAsync(instagramPin.toString()) and :51 tiktokPin features/home/components/OfferVoucher.tsx:119,197 await Clipboard.setStringAsync(voucherCode) features/offers/components/OfferVoucher.tsx:125,244 await Clipboard.setStringAsync(voucherCode) No EXTRA_IS_SENSITIVE flag and no post-copy clipboard clear; uses expo-clipboard global clipboard
Attack scenario
A co-resident app polls the clipboard after the user copies a verification PIN and uses it to complete social-account verification, or reads a copied voucher code and redeems it first.
Impact

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.

Remediation
Avoid copying genuinely sensitive values to the global clipboard, or set the Android sensitive-content flag (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.
Verifier note

Confirmed all six call sites by grep and reading Step3Verification.tsx.

AA-20

Insecure PRNG (Math.random) used to generate FCM-style device tokens

P3 M10: Insufficient Cryptography / MASVS-CRYPTO-2 CWE-330 CVSS 3.1 confirmed
Description

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.

Evidence
features/notifications/api/fcm.ts:60-70 generateTestFCMToken(): for (let i=0;i<152;i++){ token += chars.charAt(Math.floor(Math.random()*chars.length)); } fcm.ts:59 comment labels it a 'test FCM token for investigation', but it is an exported function shipped in the bundle and IS invoked in production paths shared/utils/tokenRefresh.ts:30,126 and shared/utils/formHandler.ts:421 call generateTestFCMToken() to mint the device token actually saved and registered
Attack scenario
An attacker who can model the Math.random() sequence could predict or collide device tokens used in registration, aiding push/device-identity spoofing in conjunction with a captured JWT.
Impact

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.

Remediation
Replace 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.
Verifier note

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.

AA-21

Unsanitized FCM push payload field interpolated into Expo Router navigation path

P3 M4: Insufficient Input/Output Validation CWE-20 CVSS 3.1 likely
Description

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.

Evidence
features/notifications/hooks/useNotifications.ts:82-83 if (data?.type === 'offer' && data?.offerId) router.push(`/offer/${data.offerId}?origin=notifications`) — data.offerId is taken verbatim from the remote push payload and string-interpolated into the route with no type/format check features/notifications/utils/notificationNavigation.ts coerces offer_id via String() only after a > 0 numeric check, showing the safer pattern is known but not applied in useNotifications.ts
Attack scenario
An actor with push-send capability sends a notification whose offerId is '1/../profile?foo=bar'; tapping it navigates the user to an unintended internal screen with attacker-chosen params.
Impact

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.

Remediation
Coerce offerId to a positive integer before navigation (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.
Verifier note

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.

Remediation roadmap

Immediate
Treat the committed Intercom Access Token as a live breach.
  • AA-01: Rotate/revoke the Intercom Access Token in the Intercom dashboard now; remove 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.
  • AA-14: Verify the Firebase AIzaSy... key has GCP application + API restrictions; stop tracking google-services.json; add gitleaks pre-commit/CI with a tok: rule.
  • Cross-cutting: Fix the 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.
This week
Close the high-severity auth, transport, and WebView gaps.
  • AA-02 / AA-09: Stop overloading /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().
  • AA-03: Remove client-supplied user_id from verify-otp; resolve identity server-side from email+OTP only; switch to opaque non-sequential IDs.
  • AA-06: Harden the WebView — javaScriptEnabled={false}, originWhitelist=['https://*.alist.ae'], onShouldStartLoadWithRequest host gating, setSupportMultipleWindows={false}.
  • AA-07: Move to a 6-digit OTP and add server-side attempt caps, TTL, single-use invalidation, and resend cooldown.
  • AA-10 / AA-11: Stop returning the OTP in the API response; remove all OTP/PII/OAuth console.* and OTP-in-toast statements; gate debug on __DEV__.
This month
Rebuild supply-chain and transport integrity.
  • AA-05: Commit a fresh lockfile, un-gitignore it, switch all installs (incl. EAS) to npm ci.
  • AA-08: Stand up CI (Bitbucket/GitHub Actions) running npm ci + npm audit --audit-level=high/snyk test + lint + typecheck; add Dependabot/Renovate and per-release SBOM scanning.
  • AA-04: Add SPKI public-key pinning for *.alist.ae via expo-build-properties + network_security_config / TrustKit, with a backup pin and CT.
  • AA-12: In buildUrl reject non-https schemes + enforce a host allowlist; add Android cleartext=false and iOS ATS; validate EXPO_PUBLIC_API_BASE_URL at startup.
  • AA-15: Replace the archived react-native-render-html or sanitize/allowlist anchors and server-side-sanitize HTML.
Hardening
Defense-in-depth for residual lower-severity items.
  • AA-13: Add expo-screen-capture / FLAG_SECURE on OTP, verification-code, and voucher screens; iOS background blur.
  • AA-16: Stop silently swallowing storage errors; add Sentry/Crashlytics and verify each logout clear.
  • AA-17: Return uniform non-committal responses for existence checks; rate-limit validate/OTP endpoints.
  • AA-18: Enforce PKCE, a random verified state, and event.origin validation in the web YouTube OAuth flow.
  • AA-19: Set the Android sensitive-clipboard flag and auto-clear copied PINs/voucher codes.
  • AA-20: Replace Math.random() device-token generation with expo-crypto / the real Expo push token.
  • AA-21: Coerce offerId to a positive integer and use structured router params before navigating from push payloads.

Code quality

Run / deploy

Local setup

# 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

Environment

Deployment hints

Builds run through EAS Build (project id 05f9a62a-4bc2-4bb9-bd51-c78b52e52ad8). Six profiles exist in eas.json: development, preview, preview2, preview3, preview4, productionall 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).

What to know before editing