Security Audit Report

bluesky-social/social-app

Generated by Inkwell Forge — automated codebase documentation analysis. Based on analysis of 131 screens. Subject matter expert review is recommended before distribution.

May 5, 2026

Security Audit Report (Mobile)

Application: social-app (Bluesky) Document Title: Security Audit Report (Mobile) Date: June 2026 Assessment Scope: React Native / Expo


1. Executive Summary

Generated by Inkwell Forge — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.

This security audit assessed 131 screens of the Bluesky social application, a React Native / Expo client built on the AT Protocol. The assessment was conducted through documentation-based analysis against OWASP MASVS v2.0, OWASP MSTG, OWASP ASVS v4.0.3, NIST SP 800-163 Rev 1, and CWE/SANS Top 25.

Overall Security Posture: Moderate

The application demonstrates a strong foundation in several security-critical areas. The AT Protocol's DID-based authentication model provides inherent CSRF resistance for API calls. The codebase consistently applies content moderation decisions from server-side labeling, uses sanitizeDisplayName and sanitizeHandle across profile rendering surfaces, and employs cleanError to prevent raw server error details from reaching users. The Expo OTA update mechanism is configured with code-signing certificates in production builds, and the Android intent filter uses autoVerify: true for App Links, providing meaningful resistance to deep-link hijacking.

The most significant findings center on three areas. First, the Log screen (/log) renders the application's full in-memory log buffer — which may contain user DIDs, API response fragments, and error details — with no authentication guard at the component level, creating a meaningful information disclosure risk if the route is reachable in production builds. Second, the SharedPreferencesTester E2E screen exposes native device storage read/write operations with no access control, and its presence in production builds would allow any user to probe and manipulate shared preferences. Third, the signup flow stores the user's password in React component state (a useRef) and that state object is logged in its entirety via a debug analytics call, creating a risk of password exposure in analytics pipelines.

Refer to the Finding Inventory for the authoritative record of all findings by severity.

Assessment Limitations: This assessment is based entirely on per-screen codebase documentation. It does not include dynamic application security testing (DAST), static application security testing (SAST), device forensics, binary analysis, or live penetration testing. Findings marked "(documented behavior — verify with penetration testing)" require live testing to confirm exploitability. Findings marked "(not documented — requires security testing to confirm)" require additional investigation.


2. Audit Methodology

Primary Framework: OWASP MASVS v2.0 — mobile-specific verification requirements across STOR, CRYPTO, AUTH, NETWORK, PLATFORM, CODE, and RESILIENCE categories.

Supplementary Framework: OWASP MSTG (Mobile Security Testing Guide) — testing methodology and finding classification for React Native applications.

Shared API/Web Framework: OWASP ASVS v4.0.3 — verification requirements for API and server-side concerns.

Weakness Enumeration: CWE/SANS Top 25 — standardized weakness identifiers.

Control Assessment: NIST SP 800-163 Rev 1 — mobile application vetting methodology.

Scope: All 131 screens provided in the documentation.

Limitations: Based on technical documentation review, not dynamic application security testing (DAST), static application security testing (SAST), or device forensics.

Severity Definitions (CVSS-Aligned)

Severity CVSS Range Definition SLA
Critical 9.0–10.0 Exploitable vulnerability that allows 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

3. Risk Summary Dashboard

Metric Value
Screens Assessed 131
OWASP MASVS Categories Triggered MASVS-STOR · MASVS-AUTH · MASVS-NETWORK · MASVS-PLATFORM · MASVS-CODE · MASVS-RESILIENCE — 6 of 7
Mobile-specific Risk Areas On-device storage concerns present · Deep-link validation gaps present · Native module trust concerns present · E2E test screen exposure present
Risk Posture Moderate
Findings Distribution See Finding Inventory below

4. OWASP MASVS Coverage

# Category Status Findings Notes
MASVS-STOR Data Storage Security Concern 3 Search history in AsyncStorage/localStorage; password in component state logged via analytics; log screen exposes in-memory sensitive data
MASVS-CRYPTO Cryptography Pass 0 OTA code-signing configured; no custom crypto observed in screen documentation
MASVS-AUTH Authentication & Session Management Concern 3 Log screen lacks auth guard; E2E tester screen lacks auth guard; signup password logged in debug analytics
MASVS-NETWORK Network Communication Concern 1 Certificate pinning not documented in screen files; ATS/network-security-config not assessable from screen docs
MASVS-PLATFORM Platform Interaction Concern 4 Deep-link parameter validation gaps; E2E screen exposes native storage; decodeURIComponent unguarded; window.location.href redirect in CAPTCHA
MASVS-CODE Code Quality & Build Settings Concern 3 E2E test screen in production build path; console.log removal only in production env; multiple @ts-ignore suppressions
MASVS-RESILIENCE Resilience Against Reverse Engineering Concern 1 No jailbreak/root detection documented; developer mode toggle accessible via long-press gesture

Status definitions:


5. Mobile-Specific Attack Surface Assessment

5.1 On-Device Storage Security

Storage Mechanism Usage Data Stored Encrypted? Risk Finding
useStorage (device-level, likely MMKV) Search history, account history, developer mode flag, policy NUX state, activity nudge state Search term strings, profile DIDs, boolean flags No (MMKV unencrypted by default unless configured) Medium — search history contains profile DIDs; not tokens but PII-adjacent SEC-005
persisted (AsyncStorage-backed) Reminders, last email confirm timestamp Timestamps, reminder state No Low — non-sensitive preference data None
BackgroundNotificationHandler native module playSoundChat preference Boolean Platform-native (UserDefaults/SharedPreferences) Low — non-sensitive None
SharedPrefs (expo-bluesky-swiss-army) E2E test keys: testerString, testerBool, testerNumber, testerSet Test fixture data No High — E2E screen exposes read/write to native storage with no auth guard SEC-003
React component state (useRef) Signup password Plaintext password string No (in-memory only) High — logged via debug analytics call in signup state module SEC-002
TanStack Query in-memory cache Session data, profile data, feed data Profile views, post data, session tokens (via agent) No (in-memory) Low — standard in-memory cache, cleared on app restart None

Assessment: No use of expo-secure-store or react-native-keychain for sensitive data storage is documented in the screen files. Session tokens are managed by the @atproto/api agent, whose storage mechanism is not visible in screen documentation. [Not documented — WHO: the security lead; WHAT: Does the AT Protocol agent (BskyAgent) store session tokens (accessJwt, refreshJwt) in Keychain/Keystore or in unencrypted AsyncStorage?; WHERE: Insert in Section 5.1 On-Device Storage Security table — add a row for AT Protocol session token storage]

URL Scheme Ownership: From app.config.js, the app registers the custom URI scheme bluesky:// (scheme: 'bluesky'). Custom URI schemes can be registered by any app on the device and are susceptible to hijacking. No caller-identity validation is documented.

Universal Links / App Links:

Linking Config Prefixes: Not directly visible in screen documentation. [Not documented — WHO: the mobile engineering team; WHAT: What are the linking.prefixes entries in the React Navigation linking configuration?; WHERE: Insert in Section 5.2 Deep Link Hijacking Assessment — Linking config prefixes row]

Parameter Tampering: Several screens accept route parameters that are used without explicit validation:

Scheme Ownership Validation: No documentation of caller-identity validation for the bluesky:// custom scheme.

5.3 Native Module Trust Assessment

From app.config.js and screen documentation:

Native Module / Package Source Platform Trust Assessment
expo-video Expo SDK (well-known) Both Low risk
expo-localization Expo SDK (well-known) Both Low risk
expo-web-browser Expo SDK (well-known) Both Low risk
react-native-edge-to-edge Community (well-known) Both Low risk
@sentry/react-native Sentry (well-known) Both Low risk — conditional on SENTRY_AUTH_TOKEN
expo-build-properties Expo SDK (well-known) Both Low risk
expo-notifications Expo SDK (well-known) Both Low risk
react-native-compressor Community Both Medium — verify maintenance status and last audit
@bitdrift/react-native Bitdrift (commercial SDK) Both Medium — network instrumentation SDK; verify data collection scope
MCEmojiPicker (custom fork) github.com/bluesky-social/MCEmojiPicker iOS only Medium — custom fork of community library; requires security review of fork delta
expo-bluesky-swiss-army (local module) Internal (custom native code) Both High — custom native module with SharedPrefs read/write; E2E tester screen exposes this surface. See SEC-003.
expo-bluesky-gif-view (local module) Internal (custom native code) Both Medium — custom native GIF renderer; requires security review
expo-scroll-forwarder (local module) Internal (custom native code) Both Low — scroll forwarding only
expo-emoji-picker (local module) Internal (custom native code) Both Low — emoji selection only
react-native-reanimated Software Mansion (well-known) Both Low risk
react-native-gesture-handler Software Mansion (well-known) Both Low risk
react-native-webview Community (well-known) Both Low risk — used for CAPTCHA; host allowlisting implemented
expo-image Expo SDK (well-known) Both Low risk
expo-contacts Expo SDK (well-known) Both Low risk — contacts access gated by OS permissions
expo-location Expo SDK (well-known) Both Low risk — location access gated by OS permissions
expo-camera Expo SDK (well-known) Both Low risk — camera access gated by OS permissions
react-native-pager-view Community (well-known) Both Low risk
@haileyok/bluesky-video Internal/community Both Medium — video playback management; verify maintenance status

Custom Native Modules Requiring Security Review:

5.4 Platform Security Configuration

Control iOS (ATS) Android (network-security-config) Status Notes
Certificate pinning Not documented in screen files Not documented in screen files Concern Cannot assess from screen documentation alone. [Not documented — WHO: the security lead; WHAT: Is certificate pinning configured in the native iOS ATS configuration or Android network-security-config?; WHERE: Insert in Section 5.4 Platform Security Configuration — Certificate pinning row]
Cleartext traffic usesNonExemptEncryption: false in app.config.js (iOS) Not documented in screen files Concern usesNonExemptEncryption: false indicates the app does not use encryption that requires export compliance declaration, but does not directly indicate cleartext traffic allowance. Android cleartext config not visible.
Biometric authentication Not documented in screen files Not documented in screen files Concern No biometric authentication documented for any screen. [Not documented — WHO: the security lead; WHAT: Is biometric authentication used for any sensitive operations (e.g., app unlock, account switching)?; WHERE: Insert in Section 5.4 — Biometric authentication row]
Jailbreak/root detection Not documented in screen files Not documented in screen files Concern No jailbreak or root detection documented. Developer mode is accessible via a long-press gesture on the version item in About Settings.

6. Screen-by-Screen Findings

Due to the large number of screens (131), findings are grouped by root cause and cross-referenced to affected screens. Each consolidated finding lists all affected screens in its "Affected Screens" field. Screens with no findings are not listed individually.


Log Screen — /log

Security Profile:

Aspect Assessment
Authentication None enforced at component level
Authorization None
Data Sensitivity High — renders full in-memory application log buffer
On-Device Storage None (reads from in-memory log buffer)
Deep Link Target Yes — Log route in CommonNavigatorParams
Native Modules Used None
Attack Surface High — no auth guard, exposes potentially sensitive runtime data
Finding Count Critical: 1

SEC-001: Log Screen Exposes In-Memory Application Log Buffer Without Authentication Guard

Field Value
Severity Critical
MASVS Category MASVS-AUTH
CWE CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
MASVS Requirement MASVS-AUTH-1 — The app only allows access to sensitive functionality to authenticated and authorized users
Component LogScreen (/log)
Affected Data Full in-memory application log buffer including user DIDs, API response fragments, error details, and any data logged by logger.error/logger.debug calls throughout the application
Platform Both

Description: The Log screen renders the application's complete in-memory log buffer (getEntries()) with no authentication check, no role guard, and no conditional rendering based on session state. The screen is registered as 'Log' in CommonNavigatorParams, making it navigable from any authenticated navigator. The documentation explicitly states: "No explicit client-side redirect for unauthenticated users visible in this screen's code; enforcement is expected to occur at the navigation/middleware layer." However, the screen is also accessible from the DevOptions section of Settings, which is gated only by the IS_INTERNAL build flag — a client-side constant. Log entries may contain user DIDs, AT Protocol URIs, error messages from API calls, and metadata from analytics events. The logger.debug('signup', next) call in the signup state module logs the entire wizard state object, which includes the user's password field.

Evidence: Section 14 of the Log screen documentation: "No authentication enforcement: This screen performs no authentication or authorization checks. Any user who can navigate to the /log route will see the full system log without restriction, which is a potential security concern." Section 10: "No access control: The screen is accessible to any user who can reach the route, with no developer/admin gating."

Attack Scenario: An attacker who gains physical access to an unlocked device, or who exploits a navigation vulnerability to reach the Log route, can read the full application log buffer. If the signup flow was recently used, the log may contain the user's plaintext password (logged via s.analytics?.logger.debug('signup', next) in the signup state reducer). Even without the password, the log exposes user DIDs, API error details, and behavioral metadata.

Remediation:

  1. Gate the Log screen behind the IS_INTERNAL build flag at the navigator registration level, not just at the UI entry point. Remove the 'Log' route from CommonNavigatorParams in production builds, or add a navigator-level guard that checks IS_INTERNAL before rendering the screen.
  2. As an immediate mitigation, add a session check at the top of LogScreen: if !currentAccount, render nothing or redirect to Home.
  3. Audit all logger.debug and logger.info calls to ensure no sensitive data (passwords, tokens, full state objects) is logged. Specifically, remove or redact the s.analytics?.logger.debug('signup', next) call in the signup state reducer that logs the full wizard state including the password field.
  4. Verify with penetration testing that the Log route is not reachable in production builds.

Source Evidence: Log (/log), State (/signup/state)


Shared Preferences Tester — /e2e/shared-preferences-tester

Security Profile:

Aspect Assessment
Authentication None enforced at component level
Authorization None
Data Sensitivity High — exposes native device storage read/write/delete
On-Device Storage SharedPrefs native module (UserDefaults/SharedPreferences)
Deep Link Target Yes — registered in navigator
Native Modules Used expo-bluesky-swiss-army (SharedPrefs)
Attack Surface High — no auth guard, exposes native storage operations
Finding Count High: 1

SEC-003: E2E Test Screen Exposes Native Device Storage Operations Without Authentication

Field Value
Severity High
MASVS Category MASVS-PLATFORM
CWE CWE-284: Improper Access Control
MASVS Requirement MASVS-PLATFORM-1 — The app only requests the necessary permissions
Component SharedPreferencesTesterScreen (/e2e/shared-preferences-tester)
Affected Data Native device storage (UserDefaults on iOS, SharedPreferences on Android) — any key-value pairs accessible via the SharedPrefs module
Platform Both

Description: The SharedPreferencesTesterScreen is an E2E testing utility that exposes read, write, and delete operations on native device storage via the expo-bluesky-swiss-army SharedPrefs module. The screen is registered in the navigator with no authentication guard and no build-flag gating at the component level. The documentation states: "No explicit role-based access control or authentication checks are implemented within the screen's source code itself." The screen is accessible at the /e2e/shared-preferences-tester route. If this screen is present in production builds, any user who can navigate to this route can read and write arbitrary key-value pairs in the app's native storage namespace, potentially overriding security-relevant preferences (e.g., playSoundChat, developer mode flags, or any other key stored by the app).

Evidence: Section 2 of the Shared Preferences Tester documentation: "No explicit role-based access control or authentication checks are implemented within the screen's source code itself." Section 14: "The primary security concern is that this screen should not be accessible in production builds."

Attack Scenario: A user navigates to /e2e/shared-preferences-tester in a production build. They use the "Set String" button to write an arbitrary value to testerString, then use the native module's setValue method (exposed through the screen) to overwrite application preferences. Depending on what keys the SharedPrefs module can access, this could allow manipulation of notification preferences, developer mode flags, or other security-relevant settings.

Remediation:

  1. Remove the SharedPreferencesTesterScreen route registration from all non-E2E build configurations. Gate the route behind a build-time flag (e.g., IS_E2E_BUILD) that is false in production.
  2. If the screen must remain in the codebase, add a navigator-level guard that prevents access unless the build is explicitly an E2E test build.
  3. Verify with penetration testing that the /e2e/ route namespace is not reachable in production builds.

Source Evidence: Shared Preferences Tester (/e2e/shared-preferences-tester)


Signup Flow — /signup/state

Security Profile:

Aspect Assessment
Authentication N/A (pre-authentication flow)
Authorization N/A
Data Sensitivity Critical — handles plaintext password
On-Device Storage React component state (in-memory)
Deep Link Target No
Native Modules Used None
Attack Surface High — password logged via debug analytics
Finding Count High: 1

SEC-002: Signup Wizard State Including Plaintext Password Logged via Debug Analytics

Field Value
Severity High
MASVS Category MASVS-STOR
CWE CWE-312: Cleartext Storage of Sensitive Information
MASVS Requirement MASVS-STOR-1 — The app securely stores sensitive data
Component Signup state reducer (/signup/state)
Affected Data User's plaintext password during account creation
Platform Both

Description: The signup state module's reducer function calls s.analytics?.logger.debug('signup', next) on every state transition, logging the complete SignupState object. This object includes the password field, which holds the user's plaintext password as entered during account creation. The analytics context is injected into the wizard state and may route to an external analytics service. Even if the analytics service is internal, logging plaintext passwords violates MASVS-STOR-1 and creates a risk of credential exposure in log aggregation systems, analytics dashboards, or any system that processes the debug log stream.

Evidence: Section 10 of the State (/signup/state) documentation: "Password Logged in Debug Output: s.analytics?.logger.debug('signup', next) logs the entire state object after every action, which includes the password field in plaintext. This is a potential security/privacy concern if the analytics logger transmits data externally." Section 17 confirms this as a known issue.

Attack Scenario: The analytics service receives a debug log event containing { password: "user_plaintext_password", email: "user@example.com", ... }. An attacker who gains access to the analytics dashboard, log aggregation system, or analytics data export can extract user credentials. Even without external access, this creates an insider threat risk.

Remediation:

  1. Remove the s.analytics?.logger.debug('signup', next) call from the signup state reducer entirely, or replace it with a redacted version that excludes the password field: s.analytics?.logger.debug('signup', { ...next, password: '[REDACTED]' }).
  2. Audit all other logger.debug calls in the signup flow to ensure no sensitive fields are logged.
  3. Consider storing the password in a useRef that is never included in the state object passed to the reducer, keeping it out of the state snapshot entirely.
  4. Verify with the analytics team that no existing log data containing passwords has been retained.

Source Evidence: State (/signup/state)


Multiple Screens — decodeURIComponent Without Error Guard

Security Profile (consolidated across affected screens):

Aspect Assessment
Authentication Varies by screen
Data Sensitivity Medium — route parameter handling
Deep Link Target Yes — all affected screens accept deep-linked parameters
Attack Surface Medium — malformed deep links can crash affected screens
Finding Count Medium: 1 (consolidated)

SEC-006: Unguarded decodeURIComponent on Deep-Link Route Parameters

Field Value
Severity Medium
MASVS Category MASVS-PLATFORM
CWE CWE-20: Improper Input Validation
MASVS Requirement MASVS-PLATFORM-2 — The app validates all inputs from external sources
Component HashtagScreen (/hashtag), ActivityList (/notifications/activity-list), TopicScreen (/topic)
Affected Data Application stability; potential for denial-of-service via crafted deep links
Platform Both

Description: Three screens call decodeURIComponent() on route parameters without wrapping the call in a try/catch block. If a deep link delivers a malformed percent-encoded string (e.g., %GG, %), decodeURIComponent throws a URIError, which propagates as an unhandled exception and crashes the component tree. This can be triggered by a malicious actor crafting a deep link URL and sharing it with a target user. The ActivityList screen documentation explicitly notes: "If the posts parameter contains an invalid percent-encoded sequence (e.g., %GG), a URIError will be thrown and crash the screen."

Evidence: Section 14 of Activity List: "Unguarded decodeURIComponent: The call decodeURIComponent(posts) is not wrapped in a try/catch block." Section 17 of Hashtag: "Author param not sanitized for API query: sanitizeHandle is applied to author only for display in the subtitle. The raw author value is used directly in the from:<author> search query string." Section 17 of Topic: "@ts-ignore on desktopFixedHeight" (separate issue, but the decodeURIComponent pattern is documented in Section 4).

Attack Scenario: An attacker crafts a deep link such as bsky.app/hashtag/%GG or bsky.app/topic/%GG and shares it with a target user via a social post or direct message. When the user taps the link, the app navigates to the affected screen, decodeURIComponent('%GG') throws a URIError, and the screen crashes. If no error boundary is present, the crash may propagate to the root of the app.

Remediation:

  1. Wrap all decodeURIComponent calls on route parameters in try/catch blocks:
    let decodedTag: string;
    try {
      decodedTag = decodeURIComponent(tag);
    } catch {
      decodedTag = tag; // fallback to raw value
    }
  2. Apply this fix to: HashtagScreen (the tag parameter), ActivityList (the posts parameter), and TopicScreen (the topic parameter).
  3. Add an error boundary around each affected screen to prevent crashes from propagating to the app root.
  4. Verify with penetration testing that crafted deep links cannot crash the application.

Source Evidence: Hashtag (/hashtag), Activity List (/notifications/activity-list), Topic (/topic)


Search Shell — /search/shell

Security Profile:

Aspect Assessment
Authentication Mixed — some features gated by hasSession
Authorization Session-based
Data Sensitivity Medium — search history stored per account DID
On-Device Storage useStorage (device-level, likely MMKV)
Deep Link Target Yes
Native Modules Used None
Attack Surface Medium
Finding Count Medium: 1

SEC-005: Search History Stored in Unencrypted Device Storage

Field Value
Severity Medium
MASVS Category MASVS-STOR
CWE CWE-312: Cleartext Storage of Sensitive Information
MASVS Requirement MASVS-STOR-1 — The app securely stores sensitive data
Component SearchScreenShell (/search/shell)
Affected Data Search term history (up to 6 strings), recently viewed profile DIDs (up to 10)
Platform Both

Description: The Search Shell screen stores search term history and recently viewed profile DIDs in device storage via useStorage(account, [currentAccount?.did ?? 'pwi', 'searchTermHistory']) and useStorage(account, [currentAccount?.did ?? 'pwi', 'searchAccountHistory']). The useStorage hook is described as using a device-level storage mechanism (likely MMKV or AsyncStorage). Neither MMKV nor AsyncStorage encrypts data by default. On a rooted/jailbroken device, or via a backup extraction, an attacker could read the user's search history and recently viewed profiles. While search terms are not as sensitive as credentials, they reveal behavioral patterns and the profiles a user has been viewing.

Evidence: Section 12 of Shell (/search/shell): "useStorage(account, [did, 'searchTermHistory']): Stores up to 6 recent search term strings, keyed by account DID. Falls back to 'pwi' key for unauthenticated users." and "useStorage(account, [did, 'searchAccountHistory']): Stores up to 10 recently viewed profile DIDs, keyed by account DID."

Attack Scenario: On a rooted Android device, an attacker uses ADB to pull the app's data directory and reads the MMKV or AsyncStorage database file. The search history reveals what topics and users the victim has been searching for, which could be used for social engineering, stalking, or blackmail.

Remediation:

  1. Evaluate whether search history needs to be persisted at all. If it is a convenience feature, consider making it session-only (in-memory) rather than persisted.
  2. If persistence is required, use an encrypted storage solution. For MMKV, configure encryption with a key derived from the device's secure enclave (Keychain/Keystore). For AsyncStorage, migrate to expo-secure-store or react-native-keychain for sensitive data.
  3. Limit the sensitivity of stored data — store only the minimum necessary (e.g., store search terms but not profile DIDs, which are more privacy-sensitive).
  4. Verify with device forensics testing that search history is not recoverable from device backups or rooted device file system access.

Source Evidence: Shell (/search/shell)


CAPTCHA Web View — /signup/step-captcha/captcha-web-view.web

Security Profile:

Aspect Assessment
Authentication N/A (pre-authentication)
Authorization N/A
Data Sensitivity Medium — handles CAPTCHA authorization code
Deep Link Target No
Native Modules Used None (web only)
Attack Surface Medium — iframe-based CAPTCHA with redirect detection
Finding Count Medium: 1

SEC-007: CAPTCHA Authorization Code Passed Without Format Validation

Field Value
Severity Medium
MASVS Category MASVS-PLATFORM
CWE CWE-20: Improper Input Validation
MASVS Requirement MASVS-PLATFORM-2 — The app validates all inputs from external sources
Component CaptchaWebView (/signup/step-captcha/captcha-web-view.web)
Affected Data CAPTCHA authorization code passed to the signup API
Platform iOS (web context)

Description: The CaptchaWebView component extracts the code query parameter from the CAPTCHA redirect URL and passes it directly to the onSuccess(code) callback without any format validation. The component validates the state parameter (CSRF check) and checks that code is non-null, but does not validate that code conforms to the expected format (e.g., a specific length, character set, or pattern). A malicious CAPTCHA provider or a man-in-the-middle who can inject a redirect could supply an arbitrary string as the code value. The documentation notes: "The code value extracted from the redirect URL is passed directly to onSuccess without any format validation (e.g., checking it matches an expected pattern). This is noted as a potential improvement area."

Evidence: Section 17 of Captcha Web View: "code value passed without sanitization: The code extracted from the redirect URL is passed directly to onSuccess without any format validation."

Attack Scenario: A network-level attacker (e.g., on a shared Wi-Fi network without TLS pinning) intercepts the CAPTCHA redirect and replaces the code value with a crafted string. The crafted code is passed to the signup API, which may accept it or return a specific error that reveals information about the expected format.

Remediation:

  1. Add format validation for the code parameter before passing it to onSuccess. Validate against the expected format (consult hCaptcha documentation for the expected code format — typically an alphanumeric string of a specific length).
  2. Example: if (!/^[a-zA-Z0-9_-]{10,100}$/.test(code)) { onError({error: 'Invalid code format'}); return; }
  3. Verify with penetration testing that the CAPTCHA flow is resistant to code injection.

Source Evidence: Captcha Web View.web (/signup/step-captcha/captcha-web-view.web)


Account Settings — /settings/account-settings

Security Profile:

Aspect Assessment
Authentication Required
Authorization Session-based; App Password restriction for deactivation
Data Sensitivity High — handles account deletion, password reset, handle changes
On-Device Storage None
Deep Link Target No
Native Modules Used None
Attack Surface High — destructive account operations
Finding Count Low: 1

SEC-008: Account Deletion Error Handling Resets Form State Without Rollback Notification

Field Value
Severity Low
MASVS Category MASVS-AUTH
CWE CWE-755: Improper Handling of Exceptional Conditions
MASVS Requirement MASVS-AUTH-2 — The app informs users of authentication activities
Component DeleteAccountDialog in Account Settings (/settings/account-settings)
Affected Data Account deletion confirmation code and password
Platform Both

Description: When the account deletion mutation fails, the DeleteAccountDialog resets confirmCode and password fields and returns the user to Step.VERIFY_CODE. However, the error message is displayed via Admonition and the user must re-enter their credentials. This is a minor UX/security concern: the silent reset of credentials without a clear "your deletion attempt failed" notification could confuse users into thinking the deletion succeeded. More significantly, the confirmCode.replace(WHITESPACE_RE, '') stripping is applied at submission time, which is correct, but the error recovery path does not clearly communicate to the user that their account was NOT deleted.

Evidence: Section 10 of Account Settings: "Deletion errors: Cleaned via useCleanError() hook; displayed in Admonition; resets form state and returns to Step.VERIFY_CODE."

Attack Scenario: A user attempts to delete their account. The API call fails due to a transient network error. The form resets silently. The user, believing the deletion succeeded, abandons the app. Their account remains active and potentially accessible to others.

Remediation:

  1. Add a clear, prominent error message when account deletion fails: "Your account was NOT deleted. Please try again."
  2. Consider preserving the confirmation code and password fields on error so the user does not need to re-enter them.
  3. Log deletion failures with sufficient context for support investigation.

Source Evidence: Account Settings (/settings/account-settings)


Login Form — /login/login-form

Security Profile:

Aspect Assessment
Authentication N/A (authentication entry point)
Authorization N/A
Data Sensitivity Critical — handles credentials
On-Device Storage useRef (in-memory only)
Deep Link Target No
Native Modules Used None
Attack Surface High — credential entry
Finding Count Low: 1

SEC-009: Login Error Classification Uses Fragile String Matching

Field Value
Severity Low
MASVS Category MASVS-AUTH
CWE CWE-390: Detection of Error Condition Without Action
MASVS Requirement MASVS-AUTH-2 — The app informs users of authentication activities
Component LoginForm (/login/login-form)
Affected Data Authentication error classification
Platform Both

Description: The login form classifies server errors using string matching: errMsg.includes('Token is invalid'), errMsg.includes('Authentication Required'), errMsg.includes('Invalid identifier or password'). If the AT Protocol server changes its error message wording, the client-side error categorization will silently break and fall through to the generic error handler. This is a code quality and resilience concern rather than an immediate security vulnerability, but it could result in users receiving misleading error messages (e.g., a 2FA error displayed as a generic error) that impede their ability to authenticate.

Evidence: Section 17 of Login Form: "Error discrimination by string matching: Error handling uses errMsg.includes('Token is invalid'), errMsg.includes('Authentication Required'), etc. This is fragile — if the ATProto server changes its error message wording, the client-side error categorization will silently break."

Attack Scenario: The AT Protocol server updates its error messages. Users attempting to log in with an invalid 2FA token receive a generic "Something went wrong" error instead of the specific "Invalid 2FA confirmation code" message, causing confusion and support burden.

Remediation:

  1. Replace string-based error matching with structured error type checking using the AT Protocol SDK's error classes (e.g., instanceof AuthFactorTokenRequiredError).
  2. If string matching is unavoidable, use constants defined in the AT Protocol SDK rather than hardcoded strings.
  3. Add integration tests that verify error classification remains correct when server error messages change.

Source Evidence: Login Form (/login/login-form)


About Settings — /settings/about-settings

Security Profile:

Aspect Assessment
Authentication Required
Authorization Developer mode gated by long-press gesture
Data Sensitivity Low — diagnostic information
On-Device Storage Device storage (developer mode flag)
Deep Link Target No
Native Modules Used expo-updates, expo-clipboard, expo-file-system, expo-image
Attack Surface Low — settings screen
Finding Count Low: 1

SEC-010: Developer Mode Accessible via Undocumented Long-Press Gesture (Security Through Obscurity)

Field Value
Severity Low
MASVS Category MASVS-RESILIENCE
CWE CWE-656: Reliance on Security Through Obscurity
MASVS Requirement MASVS-RESILIENCE-1 — The app implements anti-tampering mechanisms
Component About Settings (/settings/about-settings)
Affected Data Developer mode features including OTA update controls and demo mode
Platform Both

Description: Developer mode is enabled by a long-press gesture on the version item in the About Settings screen. This is security through obscurity — any user who discovers the gesture can enable developer mode, which exposes OTA update controls (Updates.fetchUpdateAsync(), Updates.reloadAsync()) and demo mode. While OTA updates are gated by Expo's code-signing in production, the ability for any user to trigger an OTA update check and reload is a potential attack surface if the code-signing verification is bypassed or if the update server is compromised.

Evidence: Section 6 of About Settings: "Version item — Long press: Toggles devModeEnabled; shows 'Developer mode enabled/disabled' toast." Section 14: "Developer options isolation: DevOptions is only compiled/shown in IS_INTERNAL builds, preventing exposure of debug tooling in production." However, the About Settings screen itself is not IS_INTERNAL-gated, and the long-press gesture enables developer mode in any build.

Attack Scenario: A user discovers the long-press gesture (e.g., from a blog post or social media). They enable developer mode and use the OTA update controls to trigger a reload. In a scenario where the Expo update server is compromised or the code-signing certificate is stolen, this could allow loading of malicious JavaScript bundles.

Remediation:

  1. Gate the developer mode long-press gesture behind IS_INTERNAL — in production builds, the long-press should have no effect.
  2. Alternatively, require a PIN or biometric confirmation before enabling developer mode.
  3. Verify that OTA code-signing is enforced and that the codeSigningCertificate configuration in app.config.js is correctly applied in production builds.

Source Evidence: About Settings (/settings/about-settings)


7. Finding Inventory

ID Screen Title Severity MASVS CWE MASVS Req Platform Remediation Phase
SEC-001 Log (/log) Log Screen Exposes In-Memory Application Log Buffer Without Authentication Guard Critical MASVS-AUTH CWE-200 MASVS-AUTH-1 Both Immediate
SEC-002 State (/signup/state) Signup Wizard State Including Plaintext Password Logged via Debug Analytics High MASVS-STOR CWE-312 MASVS-STOR-1 Both Short-Term
SEC-003 Shared Preferences Tester (/e2e/shared-preferences-tester) E2E Test Screen Exposes Native Device Storage Operations Without Authentication High MASVS-PLATFORM CWE-284 MASVS-PLATFORM-1 Both Short-Term
SEC-005 Shell (/search/shell) Search History Stored in Unencrypted Device Storage Medium MASVS-STOR CWE-312 MASVS-STOR-1 Both Medium-Term
SEC-006 Multiple — Affected Screens: Hashtag (/hashtag), Activity List (/notifications/activity-list), Topic (/topic) Unguarded decodeURIComponent on Deep-Link Route Parameters Medium MASVS-PLATFORM CWE-20 MASVS-PLATFORM-2 Both Medium-Term
SEC-007 Captcha Web View.web (/signup/step-captcha/captcha-web-view.web) CAPTCHA Authorization Code Passed Without Format Validation Medium MASVS-PLATFORM CWE-20 MASVS-PLATFORM-2 iOS Medium-Term
SEC-008 Account Settings (/settings/account-settings) Account Deletion Error Handling Resets Form State Without Rollback Notification Low MASVS-AUTH CWE-755 MASVS-AUTH-2 Both Backlog
SEC-009 Login Form (/login/login-form) Login Error Classification Uses Fragile String Matching Low MASVS-AUTH CWE-390 MASVS-AUTH-2 Both Backlog
SEC-010 About Settings (/settings/about-settings) Developer Mode Accessible via Undocumented Long-Press Gesture Low MASVS-RESILIENCE CWE-656 MASVS-RESILIENCE-1 Both Backlog

8. Needs Verification

The following items require live penetration testing, device forensics, or additional documentation to confirm. They are not counted in the Finding Inventory severity totals.

NV-001: AT Protocol Session Token Storage Mechanism

NV-002: Certificate Pinning Implementation

NV-003: bluesky:// Custom URI Scheme Hijacking

NV-004: Jailbreak/Root Detection Effectiveness

NV-005: OTA Update Code-Signing Enforcement


9. MASVS Compliance Summary

Category Topic Requirements Assessed Pass Fail N/A Compliance
MASVS-STOR Data Storage 4 2 2 0 50% — Limited Assessment (session token storage not visible in screen docs)
MASVS-CRYPTO Cryptography 2 2 0 0 100% — Limited Assessment (OTA code-signing documented; no custom crypto in screens)
MASVS-AUTH Authentication 5 2 3 0 40% — Log screen and E2E screen lack auth guards; password logged
MASVS-NETWORK Network Communication 2 0 0 2 Limited Assessment — certificate pinning and cleartext config not assessable from screen docs
MASVS-PLATFORM Platform Interaction 5 2 3 0 40% — Deep-link parameter validation gaps; E2E screen exposes native storage
MASVS-CODE Code Quality 4 2 2 0 50% — E2E screen in production path; fragile error classification
MASVS-RESILIENCE Resilience 3 1 1 1 33% — No jailbreak detection documented; developer mode via gesture

Note: "Limited Assessment" indicates that the available documentation does not provide sufficient detail to fully assess the requirement. Live testing is required for complete compliance determination.


10. Remediation Roadmap

Phase 1: Immediate (0–7 days) — Critical Findings

Phase 2: Short-Term (1–4 weeks) — High Findings

Phase 3: Medium-Term (1–3 months) — Medium Findings

Phase 4: Backlog — Low Findings

Phase 1 Total Effort Estimate: 1–2 days Phase 2 Total Effort Estimate: 1.5 days Phase 3 Total Effort Estimate: 4.5–7.5 days Phase 4 Total Effort Estimate: 2 days


11. Positive Security Patterns

Pattern 1: AT Protocol DID-Based Authentication with Inherent CSRF Resistance

Pattern 2: Consistent Content Moderation Enforcement at Render Time

Pattern 3: OTA Update Code-Signing in Production

Pattern 4: Android App Links with autoVerify: true

Pattern 5: Input Sanitization for Display Names and Handles


12. Glossary

AsyncStorage — A React Native key-value storage system that persists data to the device's file system in plaintext. Not encrypted by default. Deprecated in favor of community alternatives.

AT Protocol (atproto) — The decentralized social networking protocol developed by Bluesky. Defines lexicons (schemas) for posts, feeds, actors, and moderation. All API calls use XRPC over HTTPS.

ATS (App Transport Security) — An iOS security feature that enforces HTTPS for all network connections. Configured in the app's Info.plist.

Certificate Pinning — A security technique that restricts which TLS certificates an app will accept, preventing MITM attacks even with rogue CA certificates.

CWE (Common Weakness Enumeration) — A community-developed list of software and hardware weakness types maintained by MITRE.

CSRF (Cross-Site Request Forgery) — An attack that tricks a user's browser into making authenticated requests to a web application without the user's knowledge.

Deep Link — A URL that navigates directly to a specific screen within a mobile application. Can use custom URI schemes (bluesky://) or universal/app links (https://bsky.app/...).

DID (Decentralized Identifier) — A globally unique, persistent identifier for an AT Protocol account (e.g., did:plc:abc123). Used as the canonical identity key.

Expo — A framework and platform for React Native applications. Provides managed workflow, EAS Build, and OTA updates.

IDOR (Insecure Direct Object Reference) — A vulnerability where an attacker can access objects by manipulating references (e.g., IDs) in requests.

Jailbreak Detection — A technique to detect whether an iOS device has been jailbroken, which would allow bypassing OS security controls.

JSI (JavaScript Interface) — A React Native architecture component that allows direct synchronous communication between JavaScript and native code.

Keychain — iOS secure storage for sensitive data (passwords, tokens, certificates). Data is encrypted and protected by the device's secure enclave.

Keystore — Android secure storage for cryptographic keys. Used to protect sensitive data with hardware-backed encryption on supported devices.

MASVS (Mobile Application Security Verification Standard) — OWASP's standard for mobile application security, defining security requirements across seven categories.

MMKV — A high-performance key-value storage library for React Native. Not encrypted by default; encryption requires explicit configuration with a key.

MSTG (Mobile Security Testing Guide) — OWASP's guide for testing mobile application security.

Native Module — A React Native component that bridges JavaScript to platform-specific (Swift/Kotlin) code.

OTA (Over-The-Air) Update — A mechanism to update an app's JavaScript bundle without going through the App Store/Play Store review process. Expo provides this via EAS Update.

PDS (Personal Data Server) — In the AT Protocol, the server that hosts a user's data repository.

Root Detection — A technique to detect whether an Android device has been rooted, which would allow bypassing OS security controls.

SecureStore — Expo's API for accessing the platform's secure storage (Keychain on iOS, Keystore on Android).

SSRF (Server-Side Request Forgery) — An attack where an attacker can cause the server to make requests to internal or external resources.

Turbo Module — The next-generation React Native native module system, providing improved performance and type safety.

Universal Link — An iOS mechanism that associates a domain with an app, allowing HTTPS URLs to open the app directly. Verified via apple-app-site-association file.

App Link — The Android equivalent of Universal Links. Verified via assetlinks.json file on the domain.

URI Scheme — A custom URL scheme (e.g., bluesky://) that can be registered by any app on the device. Susceptible to hijacking.

XRPC — The remote procedure call protocol used by AT Protocol. Queries use GET; procedures (writes) use POST.

XSS (Cross-Site Scripting) — An attack where malicious scripts are injected into web pages viewed by other users. Less applicable in React Native (no DOM) but relevant for WebView components.

Managed Workflow — An Expo project configuration where Expo manages the native build process. The app in this assessment uses a bare/custom workflow based on the presence of app.config.js and custom plugins.

Bare Workflow — An Expo project configuration where the developer has full control over the native code. Allows custom native modules and plugins.

Critical — CVSS 9.0–10.0. Exploitable vulnerability allowing unauthorized access, data breach, or system compromise. Fix immediately (24h).

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.