myowjaYOY/social-app
April 19, 2026
Application: social-app (Bluesky / AT Protocol Mobile Client) Document Title: Security Audit Report (Mobile) Date: April 2026 Assessment Scope: React Native / Expo (Managed & Bare Workflow) Prepared By: DocAgent — Automated Codebase Documentation Analysis Distribution: Security Engineering, Mobile Engineering, Compliance
Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
This Security Audit Report evaluates three screens of the social-app mobile application — a React Native / Expo client for the Bluesky / AT Protocol social network — against the OWASP Mobile Application Security Verification Standard (MASVS) v2.0, OWASP ASVS v4.0.3, NIST SP 800-163 Rev 1, and CWE/SANS Top 25. The overall security posture of the assessed screens is Moderate. The screens are architecturally simple gate and empty-state UIs with limited attack surface; no on-device sensitive data storage, no text input fields, and no complex authorization logic are present in the assessed components. Several meaningful security weaknesses are identified, primarily in the areas of missing client-side rate limiting, silent error handling, and information disclosure through error messages.
The three highest-risk findings are: (1) SEC-001 — Missing Rate Limiting on Account Reactivation, where the absence of button disabling during an in-flight activateAccount XRPC call allows rapid repeated submissions that could facilitate account-state manipulation or denial-of-service against the PDS endpoint; (2) SEC-004 — Deactivated Route Accessible as Deep Link Without Session Validation, where the /deactivated route is reachable via the app's registered URL scheme without a component-level session guard, creating a potential for confused-deputy navigation if the session layer fails to intercept; and (3) SEC-007 — Silent Mutation Failure Masks Authorization Errors on Feed Write, where the fire-and-forget pattern on useAddSavedFeedsMutation in the No Following Feed screen silently discards 401 Unauthorized responses, leaving users with no indication that their session has expired and that the feed was not saved.
Across all three screens, 11 findings were identified: 0 Critical, 3 High, 5 Medium, 2 Low, and 1 Info. Mobile-specific categories (on-device storage, deep links, native modules) account for 3 findings. No direct on-device storage misuse (e.g., tokens in AsyncStorage) was identified in the assessed screens; the primary mobile-specific risks relate to deep-link exposure and the absence of documented platform security controls (certificate pinning, jailbreak detection). This assessment is based exclusively on technical documentation review and does not constitute dynamic application security testing, static binary analysis, or device forensics. All findings marked "(documented behavior — verify with penetration testing)" or "(not documented — requires security testing to confirm)" require live validation before remediation priority is finalized.
OWASP MASVS v2.0 — Mobile-specific verification requirements across seven categories: MASVS-STOR (Data Storage), MASVS-CRYPTO (Cryptography), MASVS-AUTH (Authentication & Session Management), MASVS-NETWORK (Network Communication), MASVS-PLATFORM (Platform Interaction), MASVS-CODE (Code Quality & Build Settings), and MASVS-RESILIENCE (Resilience Against Reverse Engineering).
OWASP MSTG (Mobile Security Testing Guide) — Testing methodology and finding classification for React Native applications, including guidance on Expo Router deep-link handling, AsyncStorage misuse patterns, and native module vetting.
OWASP ASVS v4.0.3 — Verification requirements for API and server-side concerns, applied to the AT Protocol XRPC calls documented in each screen.
CWE/SANS Top 25 — Standardized weakness identifiers used to classify each finding.
NIST SP 800-163 Rev 1 — Mobile application vetting methodology used to assess native module trust and platform security control completeness.
All three screens provided in the documentation:
/deactivated (Deactivated screen)/feeds/no-following-feed (No Following Feed screen)/feeds/no-saved-feeds-of-any-type (No Saved Feeds Of Any Type screen)This assessment is based on technical documentation review only. It does not include:
Findings are derived from documented patterns, architecture descriptions, and stated behaviors. Where documentation is silent on a security control, the finding is marked accordingly.
| Severity | CVSS Range | Definition | SLA |
|---|---|---|---|
| Critical | 9.0–10.0 | Exploitable vulnerability allowing unauthorized access, data breach, or system compromise | Fix immediately (24h) |
| High | 7.0–8.9 | Significant vulnerability with high probability of exploitation and serious impact | Fix within 1 week |
| Medium | 4.0–6.9 | Moderate vulnerability requiring specific conditions to exploit | Fix within 30 days |
| Low | 0.1–3.9 | Minor security weakness with limited exploitability or impact | Fix within 90 days |
| Info | 0.0 | Security best practice recommendation or hardening suggestion | Consider for future work |
| Metric | Value |
|---|---|
| Total Findings | 11 |
| Critical | 0 |
| High | 3 |
| Medium | 5 |
| Low | 2 |
| Info | 1 |
| Screens Assessed | 3 |
| OWASP MASVS Categories Triggered | 5/7 |
| On-Device Storage Findings | 0 |
| Deep Link Findings | 2 |
| Native Module Findings | 0 |
| # | Category | Status | Findings | Notes |
|---|---|---|---|---|
| MASVS-STOR | Data Storage Security | Pass | 0 | No on-device sensitive data storage documented in assessed screens; no AsyncStorage, MMKV, or SecureStore usage present |
| MASVS-CRYPTO | Cryptography | Pass | 0 | No cryptographic operations performed in assessed screens; TID generation is identifier creation, not cryptography |
| MASVS-AUTH | Authentication & Session Management | Concern | 3 | Session guard relies entirely on upstream shell layer with no component-level fallback; rate limiting absent on reactivation; silent auth-error swallowing on feed mutations |
| MASVS-NETWORK | Network Communication | Concern | 2 | Certificate pinning and cleartext traffic policy not documented for assessed screens; XRPC error handling exposes internal error strings |
| MASVS-PLATFORM | Platform Interaction | Concern | 3 | Deep-link routes accessible without component-level session validation; no jailbreak/root detection documented; platform security configuration not documented |
| MASVS-CODE | Code Quality & Build Settings | Concern | 2 | Non-null assertion on session object; misleading error message copy; fire-and-forget async patterns without error handling |
| MASVS-RESILIENCE | Resilience Against Reverse Engineering | Concern | 1 | No jailbreak/root detection, no anti-tampering, no certificate pinning documented at any assessed screen level (not documented — requires security testing to confirm at app level) |
No direct on-device storage operations (AsyncStorage, MMKV, SecureStore, react-native-keychain) are performed within any of the three assessed screen components. Section 12 of the Deactivated screen documentation explicitly states: "No AsyncStorage, MMKV, or SecureStore reads/writes occur directly in this component." The No Following Feed and No Saved Feeds Of Any Type screens similarly document no client-side persistence.
| Storage Mechanism | Usage | Data Stored | Encrypted? | Risk | Finding |
|---|---|---|---|---|---|
AsyncStorage |
Not used in assessed screens | N/A | No (plain-text) | N/A | None |
react-native-mmkv (MMKV) |
Not used in assessed screens | N/A | No by default | N/A | None |
react-native-keychain / expo-secure-store |
Not used in assessed screens | N/A | Yes (Keychain/Keystore) | N/A | None |
Session state (#/state/session) |
Used indirectly via useSession hook |
currentAccount, accounts list including DIDs and handles |
Not documented — storage mechanism of session state is outside assessed screen scope | Medium (if session tokens stored in AsyncStorage) | [Not documented — WHO: mobile engineering lead; WHAT: What storage mechanism does #/state/session use to persist session tokens and account credentials across app restarts — specifically, does it use AsyncStorage, MMKV, SecureStore, or Keychain?; WHERE: Insert in Section 5.1 On-Device Storage Security table, Session state row, Encrypted? column] |
Note: The session state layer (
#/state/session) is referenced by all three assessed screens but its internal storage implementation is outside the scope of the provided documentation. The security of token persistence depends entirely on that layer's implementation. If session tokens are stored in unencrypted AsyncStorage, a High finding would apply across all screens that consumeuseSession.
All three assessed screens are registered as Expo Router file-based routes, making them automatically accessible via the app's registered URL scheme (documented as defined in app.json under scheme).
URL Scheme Ownership:
[Not documented — WHO: the DevOps/release engineering team; WHAT: What is the registered custom URI scheme for the app (e.g., bsky://, bluesky://) as defined in app.json?; WHERE: Insert in Section 5.2 Deep Link Hijacking Assessment, URL Scheme Ownership paragraph]
Custom URI schemes (e.g., myapp://) can be registered by any application on the device. On Android, if another application registers the same scheme, the OS may present an app chooser or silently route the deep link to the malicious application. On iOS, the last-installed app claiming the scheme wins. This is a platform-level risk that applies to all three assessed routes.
Universal Links / App Links:
[Not documented — WHO: the mobile engineering lead; WHAT: Are Universal Links (iOS apple-app-site-association) and Android App Links (assetlinks.json) configured for the app's deep-link routes? If so, which routes are covered?; WHERE: Insert in Section 5.2 Deep Link Hijacking Assessment, Universal Links / App Links paragraph]
Assessed Routes and Deep-Link Risk:
| Route | Deep-Link Target | Parameter Validation | Scheme Type | Hijacking Risk | Finding |
|---|---|---|---|---|---|
/deactivated |
Yes — accessible via app URL scheme | No route params consumed; data from session state only | Custom URI scheme (inferred from Expo Router file-based routing — verify against app.json scheme configuration) |
Medium — route reachable without deactivated session; shell layer is sole guard | SEC-004 |
/feeds/no-following-feed |
Yes — accessible via app URL scheme | No route params consumed | Custom URI scheme (inferred from Expo Router file-based routing — verify against app.json scheme configuration) |
Low — no sensitive action triggered on mount; action requires authenticated session | SEC-005 |
/feeds/no-saved-feeds-of-any-type |
Yes — accessible via app URL scheme | No route params consumed | Custom URI scheme (inferred from Expo Router file-based routing — verify against app.json scheme configuration) |
Medium — destructive overwrite mutation accessible via deep link; no confirmation guard | SEC-005 |
Parameter Tampering: None of the three screens consume route parameters from useLocalSearchParams() or useGlobalSearchParams(). All data is sourced from session state or hardcoded constants. This eliminates parameter-injection risk at the component level for the assessed screens.
Scheme Ownership Validation: No inter-app caller identity verification is documented for any of the three routes. The app does not validate the originating application identity when handling deep links (not documented — requires security testing to confirm at the OS routing level).
[Screenshot: deep-link flow diagram showing custom URI scheme routing to /deactivated, /feeds/no-following-feed, and /feeds/no-saved-feeds-of-any-type]
The following native modules are referenced across the three assessed screens:
| Native Module / Package | Screen | Source | Platform | Trust Assessment |
|---|---|---|---|---|
react-native-safe-area-context |
Deactivated | Well-known published package (Expo ecosystem) | iOS + Android | Trusted — widely used, actively maintained |
@tanstack/react-query |
Deactivated, No Saved Feeds | Well-known published package | iOS + Android | Trusted — no native code; pure JS |
@lingui/core, @lingui/react |
All three screens | Well-known published package | iOS + Android | Trusted — no native code; pure JS |
@atproto/common-web (TID) |
No Saved Feeds | First-party Bluesky SDK | iOS + Android | Trusted — first-party; verify supply chain |
react-native (core) |
All three screens | Meta / React Native core | iOS + Android | Trusted |
No custom native modules of unknown origin (NativeModules.X with custom bridging) are documented in any of the three assessed screens. No deprecated or unmaintained packages shipping native binaries are identified in the assessed screen documentation.
Note: The
#/state/sessionlayer,useAccountSwitcher, anduseLoggedOutViewControlshooks referenced by the Deactivated screen may internally depend on additional native modules (e.g.,react-native-keychain,expo-secure-store,react-native-mmkv) that are outside the scope of the provided documentation. Those dependencies should be assessed separately.
| Control | iOS (ATS) | Android (network-security-config) | Status | Notes |
|---|---|---|---|---|
| Certificate pinning | Not documented in assessed screens | Not documented in assessed screens | Concern | No certificate pinning configuration is referenced in any of the three screen documents. [Not documented — WHO: the security lead; WHAT: Is certificate pinning configured and tested on both iOS and Android for XRPC calls to the PDS? If so, which library is used (e.g., react-native-ssl-pinning, OkHttp pinning)?; WHERE: Insert in Section 5.4 Platform Security Configuration table, Certificate pinning row] |
| Cleartext traffic | Not documented | Not documented | Concern | ATS and Android network-security-config cleartext policy not referenced. (not documented — requires security testing to confirm) |
| Biometric authentication | Not used in assessed screens | Not used in assessed screens | N/A | No biometric auth is documented for the three assessed screens; may be present elsewhere in the app |
| Jailbreak/root detection | Not documented in assessed screens | Not documented in assessed screens | Concern | Section 14 of the Deactivated screen explicitly states: "No biometric auth, jailbreak detection, or certificate pinning is implemented at this screen level." App-level detection is not documented. [Not documented — WHO: the security lead; WHAT: Is jailbreak (iOS) and root (Android) detection implemented at the app level, and if so, what library or mechanism is used?; WHERE: Insert in Section 5.4 Platform Security Configuration table, Jailbreak/root detection row] |
/deactivatedSecurity Profile:
| Aspect | Assessment |
|---|---|
| Authentication | Required — screen is only rendered for authenticated users with a deactivated account |
| Authorization | Session-layer enforced — no component-level authorization guard |
| Data Sensitivity | Medium — displays user handle (currentAccount?.handle); triggers account state change via XRPC |
| On-Device Storage | None — no AsyncStorage, MMKV, or SecureStore usage in this component |
| Deep Link Target | Yes — /deactivated is accessible via the app's registered URL scheme |
| Native Modules Used | react-native-safe-area-context (via useSafeAreaInsets) |
| Attack Surface | Medium — 1 XRPC API call, 4 interactive elements, session state mutation, web URL manipulation |
| Finding Count | Critical: 0, High: 2, Medium: 2, Low: 1, Info: 1 |
SEC-001: Missing Rate Limiting on Account Reactivation Button
| Field | Value |
|---|---|
| Severity | High |
| MASVS Category | MASVS-AUTH |
| CWE | CWE-799: Improper Control of Interaction Frequency |
| MASVS Requirement | MASVS-AUTH-1 — The app uses secure authentication mechanisms and does not expose them to abuse |
| Component | handleActivate / "Yes, reactivate my account" Button — app/deactivated.tsx |
| Affected Data | AT Protocol account activation state; PDS server resources |
| Platform | Both |
Description: The "Yes, reactivate my account" button is not disabled while the handleActivate async call is in-flight (pending = true). The documentation explicitly states: "There is no explicit button disabling, so rapid taps could theoretically trigger multiple concurrent calls." This allows a user — or a script controlling the UI — to dispatch multiple concurrent com.atproto.server.activateAccount XRPC calls before any response is received. While the PDS server should enforce idempotency, repeated concurrent calls may cause race conditions in session state, exhaust server-side rate limits, or produce inconsistent error states that leave the UI in an undefined condition.
Evidence: Section 10 (Error Handling & Edge Cases): "While pending is true, a Loader spinner appears inside the reactivate button. There is no explicit button disabling, so rapid taps could theoretically trigger multiple concurrent calls." Section 17 (Known Issues): "No button disabling during pending: The reactivate button is not disabled while pending is true."
Attack Scenario: A user with a deactivated account opens the app on Android. The reactivation screen is displayed. The user (or a UI automation script) taps the "Yes, reactivate my account" button 10 times in rapid succession before the first XRPC response arrives. Ten concurrent activateAccount POST requests are dispatched to the PDS. If the PDS does not enforce per-account rate limiting, this could trigger server-side errors, cause session state inconsistency when multiple agent.resumeSession() calls resolve concurrently, or exhaust the account's API quota.
Remediation:
disabled={pending} to the Button component: <Button ... disabled={pending} onPress={handleActivate}>. This is the minimal fix and is already identified in the known issues.setError(undefined) as the first line of handleActivate, before setPending(true).Source Evidence: Deactivated (/deactivated) — Sections 6, 10, 17
SEC-002: Internal Error String Comparison Creates Fragile Security Branch
| Field | Value |
|---|---|
| Severity | High |
| MASVS Category | MASVS-CODE |
| CWE | CWE-1023: Incomplete Comparison with Missing Factors |
| MASVS Requirement | MASVS-CODE-1 — The app only requests necessary permissions and does not contain sensitive data or logic that can be easily extracted |
| Component | handleActivate error handler — app/deactivated.tsx |
| Affected Data | Account reactivation security boundary (App Password vs. main password enforcement) |
| Platform | Both |
Description: The security-critical distinction between an App Password session and a full-credential session is enforced client-side by comparing the raw error message string against the literal 'Bad token scope'. The documentation states: "Error: 'Bad token scope' — Sets error to a message instructing the user to sign in with their main password instead of an App Password." This string comparison is a fragile security branch: if the AT Protocol server changes the error message wording, casing, or format in a future protocol version, the client will silently fall through to the generic error handler and display a misleading message — or worse, fail to block the reactivation attempt if the server-side enforcement is also relaxed. The security boundary between App Password and main-password sessions must not depend on client-side string matching of server error messages.
Evidence: Section 5 (Data Loading & API Calls): "Error: 'Bad token scope' — Sets error to a message instructing the user to sign in with their main password instead of an App Password." Section 8 (Business Rules): "If the user's current session was established using an App Password (a scoped credential), the server returns an error with the message 'Bad token scope'. The screen detects this specific error string and surfaces a targeted message."
Attack Scenario: A future AT Protocol PDS update changes the error code from the string 'Bad token scope' to a structured error object with a code field (e.g., { code: 'InvalidTokenScope', message: '...' }). The client-side string comparison no longer matches. The handleActivate handler falls through to the generic error branch and displays "Something went wrong, please try again" instead of the App Password warning. A user with an App Password session repeatedly retries, receiving no actionable guidance. If a future server-side regression also weakens the enforcement, the client provides no secondary defense.
Remediation:
@atproto/api SDK exposes typed error classes or error code enumerations for com.atproto.server.activateAccount, use those instead of string literals.const BAD_TOKEN_SCOPE_ERROR = 'Bad token scope' and reference the constant in the comparison, with a comment linking to the AT Protocol lexicon definition.BAD_TOKEN_SCOPE_ERROR, and a separate test for the generic fallback path.Source Evidence: Deactivated (/deactivated) — Sections 5, 8, 14
SEC-003: Misleading Error Message Undermines Security Guidance
| Field | Value |
|---|---|
| Severity | Medium |
| MASVS Category | MASVS-CODE |
| CWE | CWE-684: Incorrect Provision of Specified Functionality |
| MASVS Requirement | MASVS-CODE-1 — The app does not contain logic errors that affect security-relevant behavior |
| Component | 'Bad token scope' error message string — app/deactivated.tsx |
| Affected Data | User security guidance for App Password vs. main password distinction |
| Platform | Both |
Description: The error message displayed when a 'Bad token scope' error is received reads: "Please sign in with your main password to continue deactivating your account." However, the action being performed is reactivation, not deactivation. This is a copy error explicitly documented in Section 17. While this may appear to be a UX issue, it has a security dimension: the message is the primary mechanism by which users are informed that their App Password cannot be used for this sensitive account-state operation. A misleading message may cause users to misunderstand the security boundary, dismiss the error as a bug, or attempt workarounds that bypass the intended credential requirement.
Evidence: Section 10 (Error Handling): "Displays a specific error message: 'You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.' Note: the message says 'deactivating' but the action is reactivation — this appears to be a copy error in the source." Section 17 (Known Issues): "Copy error in error message: The 'Bad token scope' error message reads 'Please sign in with your main password to continue deactivating your account' — but the action being performed is reactivation, not deactivation."
Attack Scenario: A user with an App Password session attempts to reactivate their account. The error message instructs them to sign in with their main password to continue "deactivating" their account. The user, confused by the contradictory instruction (they want to reactivate, not deactivate), dismisses the error as a bug and does not follow the security guidance. They may attempt to use a third-party tool or workaround to bypass the credential requirement, potentially exposing their main password to a phishing surface.
Remediation:
.po files for each supported locale).'Bad token scope' error path in a test environment (e.g., using an App Password session against a local PDS) and confirming the corrected message is displayed.Source Evidence: Deactivated (/deactivated) — Sections 10, 17
Note: The development team has already identified this issue in Section 17 (Known Issues). Credit is given for awareness; the finding is included because the security impact of misleading credential guidance warrants formal tracking.
SEC-004: Deactivated Route Accessible as Deep Link Without Component-Level Session Guard
| Field | Value |
|---|---|
| Severity | Medium |
| MASVS Category | MASVS-PLATFORM |
| CWE | CWE-284: Improper Access Control |
| MASVS Requirement | MASVS-PLATFORM-2 — The app validates all input received via IPC mechanisms |
| Component | Route /deactivated — app/deactivated.tsx |
| Affected Data | Session state; account handle display; account switching UI |
| Platform | Both |
Description: The /deactivated route is registered in Expo Router's file-based routing system and is therefore reachable via the app's registered URL scheme as a deep link (e.g., bsky://deactivated). The documentation states: "The route /deactivated is technically accessible as a deep link URL given Expo Router's file-based routing, but it is not a meaningful deep-link target — arriving here without a deactivated session would result in the session layer routing the user elsewhere." The security concern is that the component itself contains no session guard — it relies entirely on the upstream shell/session layer to ensure a deactivated account is present. If the session layer has a bug, race condition, or is bypassed by a crafted deep link, the component will render with currentAccount potentially being an active (non-deactivated) account, displaying the account handle and presenting the reactivation and logout UI to an active-account user. The agent.resumeSession(agent.session!) call with a non-null assertion (SEC-006) would then execute against an already-active session.
Evidence: Section 2 (User Roles & Access Control): "There is no explicit redirect-to-login guard within the component itself — the assumption is that the session layer guarantees a valid (but deactivated) account is present when this screen is rendered." Section 9 (Navigation & Routing): "The route /deactivated is technically accessible as a deep link URL given Expo Router's file-based routing, but it is not a meaningful deep-link target."
Attack Scenario: An attacker crafts a deep link bsky://deactivated and embeds it in a phishing message sent to a Bluesky user with an active (non-deactivated) account. The user taps the link, which opens the app and navigates to /deactivated. If the session layer's routing logic has a timing window or the deep link bypasses the shell's conditional rendering, the user sees the deactivated-account UI with their handle displayed. The user, confused, taps "Yes, reactivate my account," triggering activateAccount against their already-active account. Depending on PDS behavior, this may produce an error or a no-op, but the user experience is disorienting and the logout button presents a social-engineering opportunity.
Remediation:
Deactivated component: check that currentAccount exists and that currentAccount.status === 'deactivated' (or the equivalent AT Protocol account status field). If the condition is not met, call router.replace('/') to redirect to the home screen.bsky://deactivated (or the app's registered scheme equivalent) while logged in with an active account and confirming the redirect occurs without rendering the deactivated UI.Source Evidence: Deactivated (/deactivated) — Sections 2, 9
SEC-005: Non-Null Assertion on Session Object After Activation
| Field | Value |
|---|---|
| Severity | Low |
| MASVS Category | MASVS-CODE |
| CWE | CWE-476: NULL Pointer Dereference |
| MASVS Requirement | MASVS-CODE-1 — The app does not crash or expose sensitive information due to unhandled exceptions |
| Component | agent.resumeSession(agent.session!) — handleActivate in app/deactivated.tsx |
| Affected Data | Session state; potential unhandled runtime error |
| Platform | Both |
Description: After a successful activateAccount XRPC call, the code calls agent.resumeSession(agent.session!) using a TypeScript non-null assertion (!) on agent.session. The documentation acknowledges this: "agent.resumeSession(agent.session!) uses a non-null assertion (!) on agent.session. If agent.session is somehow undefined at the point of a successful activation response, this will throw a runtime error." If agent.session is undefined at this point — due to a race condition, an unexpected agent state, or a future refactor — the non-null assertion will cause an unhandled TypeError that crashes the activation flow. The user would be left in a partially activated state with no recovery path presented.
Evidence: Section 14 (Security Considerations): "agent.resumeSession(agent.session!) uses a non-null assertion (!) on agent.session. If agent.session is somehow undefined at the point of a successful activation response, this will throw a runtime error. This is a minor fragility." Section 17 (Known Issues): "Non-null assertion on agent.session: agent.resumeSession(agent.session!) uses ! to assert agent.session is defined."
Attack Scenario: A race condition occurs during activation: the AT Protocol agent's session is cleared by a concurrent logout event (e.g., the user's session expires on the server between the activateAccount response and the resumeSession call). agent.session is undefined. The non-null assertion throws a TypeError. The finally block sets pending = false, but the unhandled error propagates up the call stack. Depending on the error boundary configuration, the app may crash or display a blank screen, leaving the user's account in an activated state on the server but with a broken client session.
Remediation:
if (agent.session) { await agent.resumeSession(agent.session); } else { setError('Session expired. Please sign in again.'); }.finally block still executes setPending(false) in all code paths.agent.session as undefined after a successful activateAccount response and confirms the error state is set rather than a TypeError being thrown.Source Evidence: Deactivated (/deactivated) — Sections 5, 14, 17
Note: The development team has already identified this issue in Section 17 (Known Issues). Credit is given for awareness.
SEC-006: Web URL Manipulation via history.pushState Before Logout
| Field | Value |
|---|---|
| Severity | Info |
| MASVS Category | MASVS-PLATFORM |
| CWE | CWE-601: URL Redirection to Untrusted Site ('Open Redirect') |
| MASVS Requirement | MASVS-PLATFORM-1 — The app only requests necessary permissions and handles platform APIs securely |
| Component | onPressLogout / history.pushState(null, '', '/') — app/deactivated.tsx (web platform only) |
| Affected Data | Browser navigation history; post-logout URL state |
| Platform | Web (IS_WEB = true) |
Description: On the web platform, the logout handler calls history.pushState(null, '', '/') before invoking logoutCurrentAccount('Deactivated'). This is documented as intentional: "On web, history.pushState(null, '', '/') is called before logoutCurrentAccount to reset the URL, since the navigator is about to unmount and cannot reliably call pushState itself." The destination is hardcoded to '/', which is safe. However, this pattern is noted as an Info-level finding because history.pushState is a platform API that manipulates browser history state. If the destination were ever made dynamic (e.g., sourced from a route parameter or query string), it would create an open redirect vulnerability. The current implementation is safe but the pattern warrants documentation for future maintainers.
Evidence: Section 3 (UI Layout): "On web, history.pushState(null, '', '/') is called before logoutCurrentAccount to reset the URL." Section 8 (Business Rules): "Because the navigator tree is about to unmount during logoutCurrentAccount, the web URL is manually reset to / via history.pushState before the logout is triggered."
Attack Scenario: (Current implementation — not exploitable.) Future maintainer modifies onPressLogout to redirect to a URL sourced from a query parameter (e.g., ?returnTo=https://evil.example.com) without validation. The history.pushState call becomes an open redirect, allowing a phishing link to redirect users to an attacker-controlled site after logout.
Remediation:
history.pushState(null, '', '/') call explicitly documenting that the destination must remain a hardcoded relative path and must never be sourced from route parameters, query strings, or external input.returnTo parameter is ever added to this flow, implement an allowlist validation that restricts the destination to known internal routes before passing it to history.pushState.Source Evidence: Deactivated (/deactivated) — Sections 3, 8
/feeds/no-following-feedSecurity Profile:
| Aspect | Assessment |
|---|---|
| Authentication | Implicitly required — useAddSavedFeedsMutation requires an authenticated session |
| Authorization | Delegated to mutation hook — no component-level auth check |
| Data Sensitivity | Low — no PII displayed; writes to user feed preferences |
| On-Device Storage | None — no AsyncStorage, MMKV, or SecureStore usage |
| Deep Link Target | Yes — /feeds/no-following-feed accessible via app URL scheme |
| Native Modules Used | None directly in this component |
| Attack Surface | Low — 1 interactive element, 1 mutation API call, no user input |
| Finding Count | Critical: 0, High: 1, Medium: 1, Low: 1, Info: 0 |
SEC-007: Silent Mutation Failure Masks Authorization Errors on Feed Write
| Field | Value |
|---|---|
| Severity | High |
| MASVS Category | MASVS-AUTH |
| CWE | CWE-390: Detection of Error Condition Without Action |
| MASVS Requirement | MASVS-AUTH-2 — The app informs the user of authentication failures and does not silently fail |
| Component | addRecommendedFeeds handler / useAddSavedFeedsMutation — app/feeds/no-following-feed.tsx |
| Affected Data | User feed preferences; session validity indication |
| Platform | Both |
Description: The addRecommendedFeeds handler calls addSavedFeeds([{ ...TIMELINE_SAVED_FEED, pinned: true }]) using mutateAsync but does not await the result and has no .catch() handler. The documentation states: "The result is fire-and-forget from the component's perspective" and "errors from the mutation (e.g., network failure, 401 Unauthorized, 500 server error) are not caught or surfaced to the user within this component." This means that if the user's session has expired (401 Unauthorized), the mutation fails silently. The onAddFeed?.() callback is invoked immediately after the mutation is dispatched — before it resolves — so the parent may navigate away or update its state as if the feed was successfully added, when in fact the write failed. The user is left in a state where they believe the following feed has been added, but it has not been persisted to their ATProto preferences.
Evidence: Section 5 (Data Loading & API Calls): "The mutation is called via mutateAsync (Promise-based), but no .then(), .catch(), or await is used in the handler — the result is fire-and-forget from the component's perspective." Section 10 (Error Handling): "No error handling is implemented within this component. If addSavedFeeds rejects (e.g., due to network failure or a server error), the error is silently swallowed at this layer." Section 17 (Known Issues): "Silent mutation failure: The addSavedFeeds call uses mutateAsync but is not awaited and has no .catch() handler."
Attack Scenario: A user's Bluesky session token expires while they are on the No Following Feed screen. The user taps "Click here to add one." The addSavedFeeds mutation fires and immediately receives a 401 Unauthorized response from the ATProto preferences API. The error is silently swallowed. The onAddFeed?.() callback fires, and the parent navigates the user away from the empty-state screen as if the feed was added. The user's feed list remains empty. The user has no indication their session expired or that the action failed. They may not discover the issue until they notice their feed list is still empty, potentially after significant time has passed.
Remediation:
async function and await the addSavedFeeds call within a try/catch block:
const addRecommendedFeeds = async (e: GestureResponderEvent) => {
e.preventDefault();
try {
await addSavedFeeds([{ ...TIMELINE_SAVED_FEED, pinned: true }]);
onAddFeed?.();
} catch (err) {
// Surface error to user — e.g., toast notification or inline error message
Toast.show('Failed to add feed. Please try again.');
}
return false as const;
};onAddFeed?.() callback invocation to after the await resolves successfully, so the parent only navigates away on confirmed success.Source Evidence: No Following Feed (/feeds/no-following-feed) — Sections 5, 6, 10, 17
Note: The development team has already identified this issue in Section 17 (Known Issues). Credit is given for awareness.
SEC-008: Duplicate Feed-Add Requests Due to Missing In-Flight Guard
| Field | Value |
|---|---|
| Severity | Medium |
| MASVS Category | MASVS-CODE |
| CWE | CWE-362: Concurrent Execution Using Shared Resource with Improper Synchronization |
| MASVS Requirement | MASVS-CODE-1 — The app does not contain logic errors that can be exploited |
| Component | addRecommendedFeeds / InlineLinkText — app/feeds/no-following-feed.tsx |
| Affected Data | User saved feeds preferences; ATProto preferences API |
| Platform | Both |
| Related Finding | SEC-010 |
Description: The InlineLinkText component has no disabled state or in-flight guard. The documentation states: "There is no spinner or disabled state on the link while the mutation is in flight. A user could theoretically tap the link multiple times before the first mutation resolves, potentially dispatching duplicate add-feed requests." Unlike the No Saved Feeds screen (SEC-010), which uses disabled={isPending} on its button, this component uses an InlineLinkText with no equivalent disabling mechanism. Multiple concurrent addSavedFeeds calls could result in duplicate feed entries being written to the user's ATProto preferences, corrupting the feed list state.
Evidence: Section 10 (Error Handling & Edge Cases): "There is no spinner or disabled state on the link while the mutation is in flight. A user could theoretically tap the link multiple times before the first mutation resolves, potentially dispatching duplicate add-feed requests. No debouncing or in-flight guard is implemented." Section 17 (Known Issues): "Duplicate tap vulnerability: There is no in-flight guard (e.g., a isLoading state disabling the link) to prevent the user from tapping the link multiple times."
Attack Scenario: A user on a slow network connection taps "Click here to add one." The mutation is dispatched but takes 3 seconds to resolve due to network latency. The user, seeing no loading indicator, taps the link two more times. Three concurrent addSavedFeeds calls are dispatched. Each call attempts to add TIMELINE_SAVED_FEED to the user's preferences. Depending on the ATProto preferences API's idempotency behavior, this may result in duplicate feed entries in the user's saved feeds list, requiring manual cleanup.
Remediation:
isLoading state variable (useState(false)) in the component, or consume the isPending state from useAddSavedFeedsMutation.isLoading to the addRecommendedFeeds handler as a guard: if (isLoading) return; at the top of the handler.InlineLinkText with a Button or Pressable component that supports a disabled prop, and set disabled={isPending} from the mutation hook.Source Evidence: No Following Feed (/feeds/no-following-feed) — Sections 5, 6, 10, 17
Note: The development team has already identified this issue in Section 17 (Known Issues). Credit is given for awareness.
SEC-009: Feed Route Accessible as Deep Link Without Authentication Guard
| Field | Value |
|---|---|
| Severity | Low |
| MASVS Category | MASVS-PLATFORM |
| CWE | CWE-284: Improper Access Control |
| MASVS Requirement | MASVS-PLATFORM-2 — The app validates all input received via IPC mechanisms |
| Component | Route /feeds/no-following-feed — app/feeds/no-following-feed.tsx |
| Affected Data | User feed preferences; ATProto preferences API |
| Platform | Both |
Description: The /feeds/no-following-feed route is accessible via the app's registered URL scheme as a deep link. The documentation states: "If an unauthenticated user somehow reached this route, the useAddSavedFeedsMutation hook would likely fail at the API layer when the mutation is triggered, but no explicit redirect or gated UI is implemented within this component itself." While the mutation failure would prevent unauthorized writes, the component renders without any authentication check, potentially displaying the empty-state UI to unauthenticated users and allowing them to trigger a mutation that fails silently (per SEC-007).
Evidence: Section 2 (User Roles & Access Control): "If an unauthenticated user somehow reached this route, the useAddSavedFeedsMutation hook would likely fail at the API layer when the mutation is triggered, but no explicit redirect or gated UI is implemented within this component itself." Section 9 (Navigation & Routing): "Deep linking to /feeds/no-following-feed is supported automatically by Expo Router's file-based routing."
Attack Scenario: An unauthenticated user receives a deep link bsky://feeds/no-following-feed in a message. They tap the link, which opens the app. The app renders the No Following Feed empty-state UI without checking for an authenticated session. The user taps "Click here to add one." The mutation fires, receives a 401 Unauthorized response, and fails silently (per SEC-007). The user sees no error and no indication they need to log in. The experience is confusing and the silent failure compounds the issue identified in SEC-007.
Remediation:
useSession()) at the top of the component. If no session is present, render a "Please sign in to continue" message or redirect to the sign-in screen.Source Evidence: No Following Feed (/feeds/no-following-feed) — Sections 2, 9
/feeds/no-saved-feeds-of-any-typeSecurity Profile:
| Aspect | Assessment |
|---|---|
| Authentication | Implicitly required — useOverwriteSavedFeedsMutation requires an authenticated session |
| Authorization | Delegated to mutation hook — no component-level auth check |
| Data Sensitivity | Medium — destructive overwrite of user's entire saved feeds configuration |
| On-Device Storage | None — no AsyncStorage, MMKV, or SecureStore usage |
| Deep Link Target | Yes — /feeds/no-saved-feeds-of-any-type accessible via app URL scheme |
| Native Modules Used | None directly in this component |
| Attack Surface | Medium — 1 interactive element, 1 destructive mutation API call, no user input, @atproto/common-web TID generation |
| Finding Count | Critical: 0, High: 0, Medium: 2, Low: 0, Info: 0 |
SEC-010: Destructive Feed Overwrite Without Confirmation or Runtime Guard
| Field | Value |
|---|---|
| Severity | Medium |
| MASVS Category | MASVS-CODE |
| CWE | CWE-693: Protection Mechanism Failure |
| MASVS Requirement | MASVS-CODE-1 — The app does not contain logic errors that can be exploited to cause unintended behavior |
| Component | addRecommendedFeeds / useOverwriteSavedFeedsMutation — app/feeds/no-saved-feeds-of-any-type.tsx |
| Affected Data | User's entire saved feeds configuration (ATProto actor preferences) |
| Platform | Both |
Description: The "Use recommended" button triggers useOverwriteSavedFeedsMutation, which replaces the user's entire saved feeds list with a hardcoded set of recommended feeds. The documentation explicitly flags this: "The calling context is responsible for ensuring this component is only rendered when the user's saved feeds list is genuinely empty. Rendering this component when the user has existing feeds and allowing them to press the button would silently destroy their current feed configuration." There is no runtime guard within the component to verify the feeds list is actually empty before executing the overwrite, no confirmation dialog, and no undo mechanism. The protection is purely by convention — a JSDoc comment instructs callers to only render this component when feeds are empty — but this is not enforced at runtime. A rendering bug in a parent component, or a deep-link navigation to this route, could expose the destructive button to users who have existing feeds.
Evidence: Section 8 (Business Rules): "The calling context is responsible for ensuring this component is only rendered when the user's saved feeds list is genuinely empty. Rendering this component when the user has existing feeds and allowing them to press the button would silently destroy their current feed configuration." Section 14 (Security Considerations): "The most significant security/data-integrity concern is the overwrite semantics of the mutation. If this component is rendered incorrectly (i.e., when the user does have saved feeds), pressing the button will silently destroy the user's existing feed configuration with no confirmation dialog or undo mechanism." Section 17 (Known Issues): "No confirmation dialog for destructive action."
Attack Scenario: A parent component has a rendering bug where it mounts NoSavedFeedsOfAnyType while the user's saved feeds list is non-empty (e.g., due to a race condition where the feeds query has not yet resolved). The user sees the "Use recommended" button and taps it. overwriteSavedFeeds executes, replacing the user's carefully curated list of 20 saved feeds with the 5 hardcoded recommended feeds. The user's feed configuration is permanently destroyed with no warning, no confirmation, and no undo. The user must manually re-add all their feeds.
Remediation:
addRecommendedFeeds: before calling overwriteSavedFeeds, verify that the current saved feeds list is empty. This can be done by reading the current feeds from the TanStack Query cache or by accepting a currentFeedsCount prop from the parent.currentFeedsCount > 0, display a confirmation Alert (React Native's built-in Alert.alert) before proceeding: "This will replace your current feeds with our recommendations. Are you sure?"Source Evidence: No Saved Feeds Of Any Type (/feeds/no-saved-feeds-of-any-type) — Sections 8, 14, 17
Note: The development team has already identified this issue in Section 17 (Known Issues). Credit is given for awareness.
SEC-011: Silent Failure on Destructive Overwrite Mutation
| Field | Value |
|---|---|
| Severity | Medium |
| MASVS Category | MASVS-AUTH |
| CWE | CWE-390: Detection of Error Condition Without Action |
| MASVS Requirement | MASVS-AUTH-2 — The app informs the user of authentication failures and does not silently fail |
| Component | addRecommendedFeeds / useOverwriteSavedFeedsMutation — app/feeds/no-saved-feeds-of-any-type.tsx |
| Affected Data | User saved feeds preferences; session validity indication |
| Platform | Both |
| Related Finding | SEC-007 |
Description: The addRecommendedFeeds handler awaits overwriteSavedFeeds but has no try/catch block and renders no error UI. The documentation states: "The component does not implement a local try/catch around overwriteSavedFeeds. Error propagation is delegated to the mutation hook's internal error state. No error UI is rendered within this component." Unlike SEC-007 (which uses a fire-and-forget pattern), this handler does await the mutation — but without error handling, an exception thrown by overwriteSavedFeeds will propagate as an unhandled promise rejection. This is a particularly significant gap because the mutation is destructive: if the write partially fails (e.g., the request is sent but the response is lost due to a network interruption), the user's feeds may be in an indeterminate state with no feedback.
Evidence: Section 10 (Error Handling & Edge Cases): "The component does not render any error UI. If overwriteSavedFeeds rejects, the error is surfaced only through the mutation hook's internal state. There is no try/catch in addRecommendedFeeds, no error Text element, and no toast/snackbar triggered on failure." Section 17 (Known Issues): "Silent failure on mutation error: The addRecommendedFeeds function awaits overwriteSavedFeeds but has no try/catch block and renders no error feedback."
Attack Scenario: A user's session token expires between the time they open the No Saved Feeds screen and the time they tap "Use recommended." The overwriteSavedFeeds mutation fires and receives a 401 Unauthorized response. The mutation hook throws an error. Because there is no try/catch in addRecommendedFeeds, the error propagates as an unhandled promise rejection. The button's isPending state returns to false (the mutation hook handles this internally), but no error message is displayed. The user sees the button become re-enabled with no explanation. They may tap it again, triggering another failed mutation, or assume the operation succeeded when it did not.
Remediation:
overwriteSavedFeeds call in a try/catch block within addRecommendedFeeds:
const addRecommendedFeeds = async () => {
onAddRecommendedFeeds?.();
try {
await overwriteSavedFeeds(
RECOMMENDED_SAVED_FEEDS.map(f => ({ ...f, id: TID.nextStr() }))
);
} catch (err) {
Alert.alert('Error', 'Failed to add recommended feeds. Please try again.');
}
};Source Evidence: No Saved Feeds Of Any Type (/feeds/no-saved-feeds-of-any-type) — Sections 5, 10, 17
Note: The development team has already identified this issue in Section 17 (Known Issues). Credit is given for awareness.
| ID | Screen | Title | Severity | MASVS | CWE | MASVS Req | Platform | Remediation Phase |
|---|---|---|---|---|---|---|---|---|
| SEC-001 | Deactivated | Missing Rate Limiting on Account Reactivation Button | High | MASVS-AUTH | CWE-799 | MASVS-AUTH-1 | Both | Short-Term |
| SEC-002 | Deactivated | Internal Error String Comparison Creates Fragile Security Branch | High | MASVS-CODE | CWE-1023 | MASVS-CODE-1 | Both | Short-Term |
| SEC-007 | No Following Feed | Silent Mutation Failure Masks Authorization Errors on Feed Write | High | MASVS-AUTH | CWE-390 | MASVS-AUTH-2 | Both | Short-Term |
| SEC-003 | Deactivated | Misleading Error Message Undermines Security Guidance | Medium | MASVS-CODE | CWE-684 | MASVS-CODE-1 | Both | Medium-Term |
| SEC-004 | Deactivated | Deactivated Route Accessible as Deep Link Without Component-Level Session Guard | Medium | MASVS-PLATFORM | CWE-284 | MASVS-PLATFORM-2 | Both | Medium-Term |
| SEC-008 | No Following Feed | Duplicate Feed-Add Requests Due to Missing In-Flight Guard | Medium | MASVS-CODE | CWE-362 | MASVS-CODE-1 | Both | Medium-Term |
| SEC-010 | No Saved Feeds Of Any Type | Destructive Feed Overwrite Without Confirmation or Runtime Guard | Medium | MASVS-CODE | CWE-693 | MASVS-CODE-1 | Both | Medium-Term |
| SEC-011 | No Saved Feeds Of Any Type | Silent Failure on Destructive Overwrite Mutation | Medium | MASVS-AUTH | CWE-390 | MASVS-AUTH-2 | Both | Medium-Term |
| SEC-005 | Deactivated | Non-Null Assertion on Session Object After Activation | Low | MASVS-CODE | CWE-476 | MASVS-CODE-1 | Both | Backlog |
| SEC-009 | No Following Feed | Feed Route Accessible as Deep Link Without Authentication Guard | Low | MASVS-PLATFORM | CWE-284 | MASVS-PLATFORM-2 | Both | Backlog |
| SEC-006 | Deactivated | Web URL Manipulation via history.pushState Before Logout |
Info | MASVS-PLATFORM | CWE-601 | MASVS-PLATFORM-1 | Web only | Backlog |
Note: Compliance percentages are calculated based on requirements assessable from the provided documentation. Requirements that cannot be assessed due to documentation gaps are marked N/A and excluded from the percentage calculation. This is a documentation-based assessment — live penetration testing is required for full compliance verification.
| Category | Topic | Requirements Assessed | Pass | Fail | N/A | Compliance |
|---|---|---|---|---|---|---|
| MASVS-STOR | Data Storage | 4 | 4 | 0 | 0 | 100% — No on-device sensitive data storage in assessed screens; session state storage mechanism requires separate assessment |
| MASVS-CRYPTO | Cryptography | 1 | 1 | 0 | 0 | 100% — TID generation is not a cryptographic operation; no cryptographic APIs used in assessed screens |
| MASVS-AUTH | Authentication | 5 | 2 | 3 | 0 | 40% — Rate limiting absent on reactivation; silent auth-error swallowing on two feed screens; session guard relies solely on upstream layer |
| MASVS-NETWORK | Network Communication | 3 | 0 | 0 | 3 | Limited Assessment — Certificate pinning, cleartext traffic policy, and TLS configuration not documented for assessed screens; requires penetration testing |
| MASVS-PLATFORM | Platform Interaction | 5 | 2 | 3 | 0 | 40% — Deep-link routes lack component-level session guards; jailbreak/root detection not documented; platform security config not documented |
| MASVS-CODE | Code Quality | 6 | 2 | 4 | 0 | 33% — Non-null assertion, fragile error string comparison, misleading error copy, missing in-flight guards, silent failure on destructive mutation |
| MASVS-RESILIENCE | Resilience | 3 | 0 | 0 | 3 | Limited Assessment — Jailbreak/root detection, anti-tampering, and certificate pinning effectiveness cannot be assessed from documentation alone; requires device-level testing |
No Critical findings identified in the assessed screens.
Phase 1 Total Effort: 0 person-days
disabled={pending} to the reactivation button and setError(undefined) at the start of handleActivate — Effort: 0.5 person-days'Bad token scope' string comparison with a typed constant or SDK error class; add unit tests for both error branches — Effort: 1 person-dayaddRecommendedFeeds in No Following Feed to an async function with try/catch; move onAddFeed?.() to after successful resolution; surface error to user — Effort: 1 person-dayPhase 2 Total Effort: ~2.5 person-days
'Bad token scope' error message copy from "deactivating" to "reactivating"; update all Lingui translation catalog files — Effort: 0.5 person-days/deactivated that checks currentAccount.status === 'deactivated' and redirects if not met — Effort: 1 person-dayInlineLinkText in No Following Feed using isPending from useAddSavedFeedsMutation; replace with Pressable + disabled prop if needed — Effort: 0.5 person-daysaddRecommendedFeeds to verify feeds list is empty before executing overwrite; add Alert.alert confirmation dialog for destructive action — Effort: 1.5 person-daystry/catch to addRecommendedFeeds in No Saved Feeds Of Any Type; surface error message to user on failure — Effort: 0.5 person-daysPhase 3 Total Effort: ~4 person-days
agent.resumeSession(agent.session!) non-null assertion with a null check and graceful error handling — Effort: 0.5 person-days/feeds/no-following-feed — Effort: 0.5 person-dayshistory.pushState call documenting the hardcoded destination requirement; add allowlist validation if returnTo parameter is ever introduced — Effort: 0.25 person-daysPhase 4 Total Effort: ~1.25 person-days
Total Estimated Remediation Effort: ~7.75 person-days
Pattern 1: App Password Security Boundary Surfaced to User
/deactivated) — handleActivate error handler'Bad token scope' error returned by the AT Protocol server when a session was established with a scoped App Password, and surfaces a targeted, actionable message instructing the user to sign in with their main password. This correctly implements a server-enforced security boundary at the UI layer, preventing App Password holders from performing sensitive account-state operations. The pattern demonstrates awareness of credential scoping and the importance of communicating security constraints to users.'Bad token scope' detection logic into a shared error-handling utility to ensure consistent behavior and to address SEC-002 (fragile string comparison).Pattern 2: Logout Source Tagging for Audit Attribution
/deactivated) — onPressLogout handlerlogoutCurrentAccount('Deactivated') passes a source string 'Deactivated' to the logout function, enabling audit logging and analytics attribution to identify which screen or context triggered the logout event. This is a lightweight but effective pattern for security monitoring — it allows the security team to distinguish between user-initiated logouts from the deactivated screen, session expiry logouts, and logouts from other contexts, which is valuable for detecting anomalous logout patterns.logoutCurrentAccount implementation persists this source string to the server-side audit log (not just client-side analytics) so that it is available for security incident investigation.Pattern 3: Post-Activation Cache Invalidation Before Session Resume
/deactivated) — handleActivate success pathqueryClient.resetQueries() is called before agent.resumeSession(). This ordering ensures that all data cached under the deactivated account state is fully cleared before the app re-enters normal operation, preventing stale or deactivation-era data from appearing in the reactivated session. This demonstrates correct understanding of the security boundary between account states and the importance of not carrying over potentially stale or unauthorized cached data across state transitions.useAccountSwitcher) to ensure that data from one account's session does not leak into another account's session context.Pattern 4: useCallback Memoization on Security-Sensitive Handlers
/deactivated) — all event handlers (onSelectAccount, onPressAddAccount, onPressLogout, handleActivate)useCallback with appropriate dependency arrays. While primarily a performance optimization, this pattern also reduces the risk of stale closure bugs in security-sensitive handlers — particularly handleActivate, which closes over currentAccount and agent. Stale closures in authentication handlers can cause security issues where an outdated session reference is used for an API call.useCallback discipline to the addRecommendedFeeds handlers in the No Following Feed and No Saved Feeds Of Any Type screens, which are currently defined inline without memoization (documented in Section 17 of both screens as known technical debt).MASVS (Mobile Application Security Verification Standard): The OWASP standard defining security requirements for mobile applications. Version 2.0 organizes requirements into seven categories: STOR, CRYPTO, AUTH, NETWORK, PLATFORM, CODE, and RESILIENCE.
MASVS-STOR: MASVS category covering secure data storage on the device. Requires that sensitive data (tokens, PII, credentials) is stored in encrypted, platform-appropriate storage (Keychain on iOS, Keystore on Android) rather than plain-text storage like AsyncStorage.
MASVS-CRYPTO: MASVS category covering cryptographic practices. Requires use of strong, up-to-date algorithms and secure key management.
MASVS-AUTH: MASVS category covering authentication and session management. Requires secure authentication mechanisms, proper session handling, and protection against authentication abuse.
MASVS-NETWORK: MASVS category covering network communication security. Requires TLS for all connections, certificate validation, and optionally certificate pinning.
MASVS-PLATFORM: MASVS category covering platform interaction security. Requires secure handling of deep links, inter-app communication, platform APIs, and permissions.
MASVS-CODE: MASVS category covering code quality and build settings. Requires absence of logic errors, debug code, and insecure coding patterns.
MASVS-RESILIENCE: MASVS category covering resilience against reverse engineering and tampering. Includes jailbreak/root detection, anti-debugging, and code obfuscation.
MSTG (Mobile Security Testing Guide): The OWASP companion guide to MASVS providing testing methodology and procedures for verifying MASVS requirements.
ASVS (Application Security Verification Standard): The OWASP standard for web application and API security verification requirements, applied here to the AT Protocol API interactions.
NIST SP 800-163 Rev 1: NIST publication on vetting the security of mobile applications, providing risk taxonomy and control assessment methodology.
CWE (Common Weakness Enumeration): A community-developed list of software and hardware weakness types maintained by MITRE. Used to classify vulnerability types in this report.
CVSS (Common Vulnerability Scoring System): A standardized framework for rating the severity of security vulnerabilities on a 0–10 scale.
CWE-284: Improper Access Control: The software does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
CWE-362: Concurrent Execution Using Shared Resource with Improper Synchronization: The program contains a code sequence that can run concurrently with other code, and the sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence operating concurrently.
CWE-390: Detection of Error Condition Without Action: The software detects a specific error condition but takes no actions to handle the error.
CWE-476: NULL Pointer Dereference: A NULL pointer dereference occurs when the application dereferences a pointer that it expects to be valid but is NULL, typically causing a crash or exit.
CWE-601: URL Redirection to Untrusted Site ('Open Redirect'): A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect.
CWE-684: Incorrect Provision of Specified Functionality: The code does not function according to its documented specification, leading to incorrect behavior that may be exploited.
CWE-693: Protection Mechanism Failure: The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product.
CWE-799: Improper Control of Interaction Frequency: The software does not properly limit the number or frequency of interactions that an actor can perform in a given time period.
CWE-1023: Incomplete Comparison with Missing Factors: The software performs a comparison that does not include all necessary factors, potentially leading to incorrect security decisions.
AsyncStorage (@react-native-async-storage/async-storage): A React Native key-value storage system that persists data to the device's file system in plain text (unencrypted). Not suitable for storing sensitive data such as authentication tokens, passwords, or PII. Equivalent to localStorage on the web.
MMKV (react-native-mmkv): A high-performance key-value storage library for React Native backed by Tencent's MMKV framework. Data is not encrypted by default; encryption must be explicitly configured. Not suitable for sensitive data without encryption enabled.
Keychain (iOS): The iOS secure credential storage system managed by the operating system. Data stored in the Keychain is encrypted and access-controlled by the OS. The appropriate storage mechanism for authentication tokens and credentials on iOS.
Keystore (Android): The Android system for storing cryptographic keys and credentials in a hardware-backed secure enclave. The appropriate storage mechanism for authentication tokens and credentials on Android.
SecureStore (expo-secure-store): An Expo library that provides an abstraction over iOS Keychain and Android Keystore, offering encrypted key-value storage suitable for sensitive data in Expo-managed and bare workflow apps.
ATS (App Transport Security): An iOS security feature that enforces secure network connections. ATS requires HTTPS for all network connections by default and can be configured in Info.plist. Exceptions must be explicitly declared and justified.
Deep link: A URL that opens a specific screen or state within a mobile application. Deep links can use custom URI schemes (e.g., bsky://) or universal/app links tied to a domain.
Universal link (iOS): An iOS mechanism that associates a domain with an app via an apple-app-site-association file hosted on the domain. Universal links are more secure than custom URI schemes because they are tied to domain ownership and cannot be hijacked by other apps.
App link (Android): The Android equivalent of iOS Universal Links. App links associate a domain with an app via an assetlinks.json file hosted on the domain, providing verified deep-link handling that resists hijacking.
URI scheme: A custom URL scheme registered by an app (e.g., bsky://, myapp://). Any app on the device can register the same scheme, making custom URI schemes vulnerable to hijacking on both iOS and Android.
Certificate pinning: A security technique where the app validates that the server's TLS certificate matches a known, expected certificate or public key, rather than trusting any certificate signed by a trusted CA. Protects against man-in-the-middle attacks using fraudulent certificates.
Jailbreak detection (iOS): Techniques used by an app to detect whether the iOS device has been jailbroken (had its security restrictions removed). Jailbroken devices may expose app data stored in the Keychain or allow code injection.
Root detection (Android): Techniques used by an app to detect whether the Android device has been rooted (had its security restrictions removed). Rooted devices may allow other apps to read the app's private data.
Native module: A module that bridges React Native JavaScript code to platform-native (Swift/Objective-C on iOS, Kotlin/Java on Android) code. Native modules can access platform APIs not available in JavaScript and ship compiled native binaries.
JSI (JavaScript Interface): The new React Native architecture's mechanism for synchronous, direct communication between JavaScript and native code, replacing the asynchronous bridge. Used by Turbo Modules.
Turbo Module: The new React Native architecture's implementation of native modules using JSI for improved performance and type safety.
AT Protocol (ATProto): The open, decentralized social networking protocol underlying Bluesky. Defines lexicons (schemas) for all data types and API operations.
PDS (Personal Data Server): The AT Protocol server that hosts a user's data and handles authentication. Each user's agent communicates with their PDS.
XRPC: The remote procedure call protocol used by AT Protocol. Procedures are either queries (GET) or procedures (POST).
App Password: A scoped credential generated by a Bluesky user for use by third-party applications. App Passwords have limited permissions and cannot perform sensitive account operations such as reactivation. Detected server-side via the 'Bad token scope' error.
DID (Decentralized Identifier): A globally unique, persistent identifier for an AT Protocol account (e.g., did:plc:abc123). Used to distinguish accounts in the multi-account session store.
TID (Time-based ID): A lexicographically sortable, time-ordered identifier format used throughout the ATProto ecosystem. Generated by TID.nextStr() from @atproto/common-web.
Feed generator: An ATProto server-side service that produces a custom ordered list of posts. Users can save feed generators to their profile.
Saved feed: A feed generator that a user has explicitly saved to their Bluesky account preferences, making it accessible from the Feeds tab.
Pinned feed: A saved feed promoted to a prominent position in the feeds list.
React Native: A framework for building native mobile applications using JavaScript and React. Renders to native platform UI components rather than a WebView.
Expo: A platform and set of tools built on top of React Native that simplifies development, build, and deployment of React Native apps.
Managed workflow: An Expo project configuration where Expo manages the native build configuration. Developers do not directly modify native iOS/Android project files.
Bare workflow: An Expo project configuration where developers have full access to and control over the native iOS and Android project files, while still using Expo libraries.
Expo Router: A file-based routing library for React Native and Expo that maps file paths in the app/ directory to navigation routes, similar to Next.js.
ASVS: Application Security Verification Standard (OWASP) ATS: App Transport Security (iOS) CWE: Common Weakness Enumeration CVSS: Common Vulnerability Scoring System DAST: Dynamic Application Security Testing DID: Decentralized Identifier IDOR: Insecure Direct Object Reference MASVS: Mobile Application Security Verification Standard (OWASP) MSTG: Mobile Security Testing Guide (OWASP) NIST: National Institute of Standards and Technology PDS: Personal Data Server (AT Protocol) PII: Personally Identifiable Information SAST: Static Application Security Testing SSRF: Server-Side Request Forgery TID: Time-based Identifier (ATProto) TLS: Transport Layer Security XSS: Cross-Site Scripting XRPC: Cross-service Remote Procedure Call (AT Protocol)
Critical (CVSS 9.0–10.0): Exploitable vulnerability allowing unauthorized access, data breach, or system compromise. Fix immediately (within 24 hours).
High (CVSS 7.0–8.9): Significant vulnerability with high probability of exploitation and serious impact. Fix within 1 week.
Medium (CVSS 4.0–6.9): Moderate vulnerability requiring specific conditions to exploit. Fix within 30 days.
Low (CVSS 0.1–3.9): Minor security weakness with limited exploitability or impact. Fix within 90 days.
Info (CVSS 0.0): Security best practice recommendation or hardening suggestion. Consider for future work.
End of Security Audit Report (Mobile) Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.