AI speed. Human accountability.
CredShields · Mobile Application Penetration Test
iOS Application Pentest
Emory Pro
Emory Pro is a product of Salico GmbH
This report presents the findings of a mobile application penetration test conducted against Emory Pro iOS application. Findings were identified and verified by the CredShields security team.
- Critical1
- High2
- Medium4
- Low11
- Info1
- Client
- Emory Pro
- Scope
- Emory Pro iOS application
- Lead
- Shashank (shashank@credshields.com)
/ Contents
Table of Contents
- 01Executive Summary
- 02Vulnerability Summary
- 03Detailed Findings
- C001Cleartext Account Password, Session Tokens, and Organisation PII Persisted in Local Network LogsCritical
- H001Long-Lived JWT Tokens Stored Unencrypted in Hive; Keychain Declared but UnusedHigh
- H002Customer PII, Signatures, and Inspection Media Cached Unencrypted (No Complete Data Protection)High
- M001App Transport Security Fully Disabled and No Certificate PinningMedium
- M002Application Documents Directory (Cleartext Tokens and PII) Exported via iTunes/Files File SharingMedium
- M003Broken Authorization in the Push-Notification Subsystem (Shared Backend)Medium
- M004Plaintext Diagnostics Stored and Retained After LogoutMedium
- L001No Jailbreak (Root) DetectionLow
- L002Improper URL Handling: https Custom Scheme and Empty Universal-Link DeclarationLow
- L003No Screenshot / App-Snapshot ProtectionLow
- L004Over-Privileged Permissions (Always-On Location, Apple Music)Low
- L005Device Fingerprint and Identity Telemetry Without Clear ConsentLow
- L006Missing App-Level Privacy Manifest (PrivacyInfo.xcprivacy) for Required-Reason APIs and Data CollectionLow
- L007Development-Flavor Build Distributed via TestFlight with Dart VM-Service (Observatory) Bonjour RemnantLow
- L008No App-Level Authentication Lock, Biometric Re-Authentication, or Session Inactivity TimeoutLow
- L009Printer Destination IP Stored in Cleartext and Insufficiently ValidatedLow
- L010No Debugger or Anti-Instrumentation (Anti-Tampering) DetectionLow
- L011No Dart Code Obfuscation (Application Logic Recoverable)Low
- I001Backend Topology Disclosed in Bundled .env AssetInfo
- 04Assessment Notes
- 05Reviewers & Signatories
/ Executive Summary
The picture at a glance
A mobile application penetration test was conducted for the Emory Pro iOS application, resulting in a total of 19 findings. These findings were categorized by severity as 1 Critical, 2 High, 4 Medium, 11 Low, and 1 Informational.
The most severe finding, a Critical vulnerability, involved the cleartext persistence of account passwords, session tokens, and sensitive organizational PII within local network logs. Two High severity findings were also identified: the unencrypted storage of long-lived JWT tokens and the caching of customer PII, signatures, and inspection media without complete data protection. These critical and high severity issues represent significant risks of sensitive data exposure and compromise, requiring immediate attention.
Following the initial assessment, a retest confirmed that 18 of the 19 identified vulnerabilities have been successfully remediated, with the remaining 1 finding acknowledged by Salico GmbH. This demonstrates a strong commitment to enhancing the security posture of the Emory Pro iOS application.
Assessment overview
This mobile application penetration test for Salico GmbH's Emory Pro iOS application involved a comprehensive security review encompassing static and dynamic analysis, examination of local data storage, and assessment of network communication and API interactions. The objective was to identify vulnerabilities within the application's client-side components and its interactions with backend services that could expose sensitive data or lead to unauthorized access.
In total, 19 findings were identified across various severity levels: one Critical, two High, four Medium, eleven Low, and one Informational. It is highly commendable that out of these 19 findings, 18 have since been confirmed as fixed during retesting, with the remaining single finding acknowledged. This demonstrates a strong commitment to addressing security vulnerabilities.
The most severe finding, rated Critical, was [C001] Cleartext Account Password, Session Tokens, and Organisation PII Persisted in Local Network Logs. This issue posed a significant risk, as it allowed for the complete compromise of user accounts and sensitive organizational data through local log access. Two high-severity issues were also uncovered, both directly related to insecure data storage practices. [H001] Long-Lived JWT Tokens Stored Unencrypted in Hive; Keychain Declared but Unused indicated a failure to properly secure authentication tokens, while [H002] Customer PII, Signatures, and Inspection Media Cached Unencrypted (No Complete Data Protection) revealed that sensitive customer information was being stored without adequate encryption, leaving it vulnerable to unauthorized access on the device.
Medium-severity findings encompassed a range of concerns, from network security to data handling and authorization. [M001] App Transport Security Fully Disabled and No Certificate Pinning highlighted a weakened transport layer, making the application susceptible to man-in-the-middle attacks. Insecure data storage continued to be a theme with [M002] Application Documents Directory (Cleartext Tokens and PII) Exported via iTunes/Files File Sharing, allowing local extraction of sensitive information, and [M004] Plaintext Diagnostics Stored and Retained After Logout, exposing debugging data. Furthermore, [M003] Broken Authorization in the Push-Notification Subsystem (Shared Backend) pointed to a potential flaw in access controls for backend functionality.
A larger group of low-severity findings, along with one informational finding, identified opportunities for further hardening and improved privacy. These included several anti-tampering gaps, such as the absence of jailbreak, debugger, or anti-instrumentation detection, and a lack of Dart code obfuscation. Privacy concerns were noted regarding over-privileged permissions, device fingerprinting without clear consent, and missing app-level privacy manifests for transparency. Other low-severity issues touched on improper URL handling, no screenshot protection, improper session management (e.g., lack of app-level authentication lock), and remnants of development-flavor builds in distribution. The informational finding, [I001] Backend Topology Disclosed in Bundled .env Asset, pointed to minor information disclosure in the application bundle. The successful remediation of nearly all identified vulnerabilities significantly enhances the overall security posture of the Emory Pro iOS application.
Audited scope
Emory Pro (iOS) - pro.emory.inspections.dev
Retested scope
Emory Pro (iOS) - pro.emory.inspections.dev
Engagement details
- Client
- Emory Pro
- Assessment type
- Grey-box Assessment
- Method
- Black-box testing
- Engagement
- Emory Pro Mobile (iOS) Security Assessment
- Report date
- July 1, 2026
- Start date
- June 5, 2026
- End date
- June 10, 2026
- Retest date
- July 1, 2026
- Total findings
- 19
- Lead
- Shashank (shashank@credshields.com)
Finding severity distribution
/ Vulnerability Summary
Every finding at a glance
Ordered by severity. Follow an ID for the full write-up.
/ Detailed Findings
19 findings across the assessment
Findings are ordered by severity.
C001 · Insecure Data Storage / Insertion of Sensitive Information into Log File (OWASP MASVS-STORAGE) · CWE-312, CWE-532, CWE-522
Cleartext Account Password, Session Tokens, and Organisation PII Persisted in Local Network Logs
Description
The iOS client runs the same network logger as the Android app, writing every API request and response (including the login request body) verbatim into an unencrypted Hive log database (logsHiveBox to Documents/logshivebox.hive) with no redaction. As proven on the byte-identical Android build, this persists the user plaintext account password, both JWT session tokens, and the full /user/organization roster (every colleague email, employee ID, and role) in clear text inside the app sandbox. On iOS these files live in the app Documents directory under the default data-protection class only (no NSFileProtectionComplete), so they are readable whenever the device is unlocked, and trivially via a backup, a jailbroken device, or an iOS-on-Mac container.
Affected Component / Screen
Documents/logshivebox.hive (logsHiveBox writer); captures POST /user/authenticate, GET /user, GET /user/organization
Evidence
logshivebox.hive contained the cleartext login body (email plus password) and the auth response with both JWTs and the org roster, reproduced live after login.
Proof of Concept
- Install and log in once on a device (the password is recorded verbatim).
- From a backup, jailbroken device, or the iOS-on-Mac data container, read the log box: strings ~/Library/Containers/pro.emory.inspections.dev/Data/Documents/logshivebox.hive | grep -i password.
- Observe the login request body in clear text followed by the auth response containing both JWTs, and the /user/organization responses listing every user email/employee-id/role.
Impact
Theft of a backup, a jailbroken device, or container access yields the victim reusable password plus live session tokens, giving full account takeover that survives token expiry. It also leaks the complete organisation user directory, enabling targeted phishing and lateral attacks. Storing credentials and third-party PII in clear text is a direct confidentiality and GDPR violation.
Remediation
- Never log request/response bodies for authentication endpoints and never log the password field; exclude /user/authenticate and /user/renew from the logger and drop password/token/refreshToken keys.
- Gate all network logging behind release-mode checks so production builds never persist bodies.
- If logs must persist, encrypt the box with a Keychain-derived key and set NSFileProtectionComplete on the file.
Retest
Retested build 756: FIXED. package:emory_mobile_app/utils/redaction.dart is compiled into 756 and the identical Dart logger was dynamically proven on Android (logshivebox encrypted, zero plaintext secrets). A fresh iOS at-rest re-pull was blocked by the new jailbreak RASP, so judged on shared-binary parity plus 756 static evidence.
H001 · Insecure Data Storage (Cleartext Storage of Credentials) - OWASP MASVS-STORAGE · CWE-312, CWE-522, CWE-922
Long-Lived JWT Tokens Stored Unencrypted in Hive; Keychain Declared but Unused
Description
The application persists the JWT access token (30-day lifetime) and refresh token (50-day lifetime) in an unencrypted Hive box (shared-box to Documents/shared-box.hive) under the keys jwt-token and emory-refresh-token, with an auth-model record of the user identity, confirmed by the iOS Dart binary containing these exact keys. The app declares the Keychain entitlement (keychain-access-groups 7NH29MB86S.pro.emory.inspections.dev) but does not use it for the auth tokens: a Keychain search returns no generic-password items, and no flutter_secure_storage/SecItem usage exists in the binary. The tokens therefore sit in clear text in Documents, recoverable from a backup, jailbroken device, or container.
Affected Component / Screen
Documents/shared-box.hive (keys jwt-token, emory-refresh-token, auth-model); declared-but-unused Keychain group 7NH29MB86S.pro.emory.inspections.dev
Evidence
shared-box.hive exposed clear-text access and refresh JWTs plus the auth-model JSON decoding to role OA, org 103, with exp values 30/50 days out; Keychain held no items.
Proof of Concept
- Log in on the device, then read the box: strings ~/Library/Containers/pro.emory.inspections.dev/Data/Documents/shared-box.hive and grep for the eyJ JWT pattern.
- Observe the clear-text access and refresh JWTs and the auth-model JSON; decode to confirm role, user_id, organization_id, exp 30/50 days out.
- Confirm the Keychain is not used: security find-generic-password -l pro.emory.inspections.dev returns item could not be found.
- Replay either token as Authorization: Bearer against https://dev-be.emory.pro (the refresh token works directly as an access bearer).
HTTP Request
GET /<any-authenticated-endpoint> HTTP/2 Host: dev-be.emory.pro Authorization: Bearer eyJ...<token-recovered-from-shared-box.hive> source: web
Impact
Backup, jailbreak, container, or file-sharing access yields a long-lived organisation-admin session with no re-authentication. Combined with the server-side refresh-token-equals-access-token weakness, the usable window extends to about 50 days. With an Org-Admin token this grants organisation-wide access.
Remediation
- Store tokens in the iOS Keychain via flutter_secure_storage with kSecAttrAccessibleWhenUnlockedThisDeviceOnly (the entitlement is already present).
- If Hive is retained for caching, encrypt the box with a Keychain-stored key and apply NSFileProtectionComplete.
- Shorten server-side token lifetimes and add a token-type claim so a refresh token cannot be replayed as an access bearer.
- Ensure logout deletes all token keys (jwt-token, emory-refresh-token, auth-model).
Retest
Retested build 756: FIXED. secure_box_key.dart plus HiveAesCipher plus flutter_secure_storage (apple_options/ios_options) are present in 756 and the keychain-access-groups entitlement is used; Android dynamic proof shows shared-box token values as ciphertext.
H002 · Insecure Data Storage (Cleartext Storage of Sensitive Information) - OWASP MASVS-STORAGE · CWE-312, CWE-311
Customer PII, Signatures, and Inspection Media Cached Unencrypted (No Complete Data Protection)
Description
The app caches business data in unencrypted Hive boxes (apidatahivebox.hive, customers-box.hive, shared-box.hive, etc.) and stores inspection media in plain files under Documents/image, video, profile, and marking. No box is opened with an encryption cipher and the app does not request NSFileProtectionComplete (no data-protection entitlement), so all of this (customer names/emails/company data, assignee handwritten-signature data, inspection records, and photos/videos) is stored in clear text and protected only by the default class (readable after first unlock).
Affected Component / Screen
Documents/apidatahivebox.hive, customers-box.hive, shared-box.hive, archived-box.hive, tasks-box.hive; Documents/image|video|profile|marking
Evidence
customers-box.hive and apidatahivebox.hive held clear-text customer emails, company names, signature data, and S3 URLs, and Documents/image held copyable inspection photos in the original build.
Proof of Concept
- Use the app to load inspections/customers so the caches populate.
- Read the boxes: strings ~/Library/Containers/pro.emory.inspections.dev/Data/Documents/customers-box.hive (and apidatahivebox.hive) and observe clear-text customer emails, company names, signature data and S3 URLs.
- List Documents/image and copy the unencrypted inspection photos.
Impact
Discloses customer/employee PII, signatures, and inspection imagery to anyone with backup, jailbreak, or container access for one device data set. This is a confidentiality and compliance (GDPR) exposure for an EU-facing inspection product.
Remediation
- Open all PII-bearing Hive boxes with a Keychain-derived AES cipher; store media in a file-protected (NSFileProtectionComplete) or encrypted container.
- Add a data-retention policy and a wipe-local-data action; clear media directories on logout.
- Request NSFileProtectionComplete as the app default data-protection class.
Retest
Retested build 758: PARTIAL - media accepted risk. flutter_secure_storage.framework is bundled and the App binary references hiveKey and .hive/.hivec cipher boxes, giving Keychain-keyed HiveAesCipher box encryption at code parity with Android (dynamically proven there). Media-to-gallery/Photos remains a formally accepted risk (company-managed devices plus company iCloud), the same decision as Android F3.
M001 · Insufficient Transport Layer Protection - OWASP MASVS-NETWORK · CWE-295, CWE-319
App Transport Security Fully Disabled and No Certificate Pinning
Description
Info.plist sets NSAppTransportSecurity to NSAllowsArbitraryLoads = true, disabling App Transport Security app-wide and permitting cleartext HTTP and weak-TLS connections to any domain rather than scoping with NSExceptionDomains. The Flutter/Dart HTTP stack performs no certificate pinning (only the stock dart:io badCertificateCallback), so the app trusts the standard CA set. A network attacker who can present a device-trusted certificate (rogue/compromised CA, MDM-installed CA, corporate inspection) can intercept the bearer JWT and refresh token, and the disabled ATS additionally allows an HTTP downgrade. The bundled .env retains commented http endpoints, confirming the app is built to talk plain HTTP in some configurations.
Affected Component / Screen
Info.plist NSAppTransportSecurity = { NSAllowsArbitraryLoads = true }; Dart dio HTTP client with default trust, no pinning
Evidence
An intercepting proxy CA captured the authenticated API traffic including Authorization: Bearer in clear, and a bearer request over plain http://dev-be.emory.pro/user returned HTTP 200 in cleartext.
Proof of Concept
- There is no UI control; reproduce at the network layer. Install an intercepting proxy CA into the device trust store and proxy the app.
- Drive any authenticated action and observe the API requests/responses (including Authorization: Bearer) in clear; no pinning rejects the proxy certificate.
- Confirm ATS is disabled: plutil -extract NSAppTransportSecurity xml1 -o - Info.plist shows NSAllowsArbitraryLoads = true.
- Confirm the downgrade is live: curl -H 'Authorization: Bearer $TOKEN' http://dev-be.emory.pro/user returns HTTP 200 with data in cleartext.
Impact
Credential and PII interception is possible under MITM (rogue Wi-Fi, compromised or over-trusting CA, MDM inspection). The cleartext capability also enables an HTTP downgrade if a host is ever misconfigured, exposing long-lived session tokens.
Remediation
- Remove NSAllowsArbitraryLoads; if an exception is genuinely required, scope it narrowly with NSExceptionDomains and keep NSExceptionRequiresForwardSecrecy/TLS 1.2+.
- Add certificate or SPKI pinning in the Dart HTTP client (a pinning interceptor or badCertificateCallback validating the backend SPKI hash).
- Remove cleartext host entries from the bundled .env.
Retest
Retested build 756: FIXED. Info.plist now sets NSAllowsArbitraryLoads=false (plus NSAllowsLocalNetworking=true for printers). Certificate pinning was skipped as an acknowledged accepted risk.
M002 · Insecure Data Storage / Sensitive Information in Externally-Accessible File - OWASP MASVS-STORAGE / MASVS-PLATFORM · CWE-538, CWE-200
Application Documents Directory (Cleartext Tokens and PII) Exported via iTunes/Files File Sharing
Description
Info.plist sets both UIFileSharingEnabled=true (iTunes/Finder file sharing) and LSSupportsOpeningDocumentsInPlace=true (Files.app in-place browsing), which expose the app entire Documents directory to anyone who can connect the device to a computer or open the Files app. That directory holds the unencrypted Hive boxes: the cleartext password/token log (logshivebox.hive, F1), the long-lived JWTs (shared-box.hive, F2), the customer/inspection PII cache (F3), and diagnostics (errorsbox.hive, F7). This removes the jailbreak/backup precondition of F1-F3/F7: a person with brief physical access to an unlocked device can copy shared-box.hive via Finder and decode the 50-day refresh token (accepted directly as a bearer) for full account takeover. Container permissions are -rw-r--r--, reflecting the absence of NSFileProtectionComplete.
Affected Component / Screen
Info.plist UIFileSharingEnabled=true, LSSupportsOpeningDocumentsInPlace=true; exposes Documents/shared-box.hive, logshivebox.hive, apidatahivebox.hive, customers-box.hive, errorsbox.hive and media dirs
Evidence
plutil confirmed both flags true and shared-box.hive was draggable out via Finder with no jailbreak, yielding a replayable refresh JWT and (from logshivebox.hive) the cleartext password.
Proof of Concept
- Confirm the flags: plutil -p Info.plist and grep for UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace, both true.
- Connect the unlocked device to Finder (or open Files.app), select the app under On My iPhone, and drag shared-box.hive out (no jailbreak).
- strings shared-box.hive and grep the eyJ JWT pattern, decode the refresh JWT, and replay as Authorization: Bearer; likewise logshivebox.hive yields the cleartext password.
Impact
The cleartext password, long-lived tokens, and customer PII were exfiltrable with only brief physical access to an unlocked device, collapsing the sandbox protection assumed by F1-F3 and enabling full account takeover with no jailbreak or backup tooling.
Remediation
- Set UIFileSharingEnabled=false and LSSupportsOpeningDocumentsInPlace=false; the app needs only to import uploads, not export its container.
- Move tokens to the Keychain (closes F2) and apply NSFileProtectionComplete to all Documents files so they are unreadable while locked and not copyable in clear.
Retest
Retested build 756: FIXED. Info.plist now sets UIFileSharingEnabled=false and LSSupportsOpeningDocumentsInPlace=false, so the Documents container is no longer exported over Finder/Files.
M003 · Broken Object/Function Level Authorization - OWASP API1/API5/API3 · CWE-639, CWE-915, CWE-285
Broken Authorization in the Push-Notification Subsystem (Shared Backend)
Description
The iOS client uses the same push-notification API as Android, which enforces no authorization. POST /notification/update-firebase-token trusts a client-supplied user_id, so any authenticated account (including the read-only Customer) can register an attacker-controlled push token under any user account (no ownership or existence check), enabling notification interception; POST /notification/send-custom-notification has no function-level authorization (any role can send custom push notifications); and POST /notification/update-firebase-token-status leaks raw MySQL result metadata. The impact is amplified by the client notification handler, which performs device-side actions on receipt of silent/data push notifications (a background data fetch and a print routine), so an attacker who registers a token can remotely trigger those background/print actions on a target device.
Affected endpoints
- POST /notification/update-firebase-token
- POST /notification/update-firebase-token-status
- POST /notification/send-custom-notification
Evidence
In the original test update-firebase-token accepted user_id 196 (admin) and even user_id 99999 (non-existent) with 200 success, and send-custom-notification returned 200 for any role.
Proof of Concept
- Authenticate as any low-privilege account.
- POST /notification/update-firebase-token with token attacker-device and user_id 196 (an admin) returns 200 New token inserted; user_id 99999 (non-existent) also succeeds, showing no validation.
- POST /notification/send-custom-notification with tokens/title/message as any role returns 200.
HTTP Request
POST /notification/update-firebase-token HTTP/2
Host: dev-be.emory.pro
Authorization: Bearer <low-priv-jwt>
Content-Type: application/json
{'token':'<attacker-device-token>','user_id':196}Impact
Before the fix, an attacker could intercept another user (including an admin) push notifications, spoof/phish via custom notifications, pollute routing data, and leak raw SQL metadata. Silent/data pushes drove client-side background fetch and printing, making those device-side actions remotely triggerable rather than merely observable.
Remediation
- Derive user_id server-side from the authenticated token on the token endpoints; never accept it from the body.
- Add function-level authorization on send-custom-notification.
- Validate referenced users exist and belong to the caller org; return generic bodies with no raw DB results.
Retest
Retested build 758 against shared dev-be, live 2026-06-30: FIXED. update-firebase-token BOLA is fixed (server derives the user from the JWT); send-custom-notification BFLA is now role-gated (OA 200, EFA/Customer 403, no-auth 401); and the update-firebase-token-status info-leak is gone. Fully closed.
M004 · Insecure Data Storage / Sensitive Information Exposure - OWASP MASVS-STORAGE · CWE-312, CWE-532, CWE-200
Plaintext Diagnostics Stored and Retained After Logout
Description
The app records errors and diagnostics in an unencrypted Hive box (errorsbox.hive) and uploads them to POST /diagnostics/errors. The records contain Dart stack traces with source-file paths, device fingerprint, build flavor, model, and session identifiers in clear text, content already observed in the iOS box pre-login. As on Android, errorsbox (and permissionsbox) are not cleared on logout (the logout routine clears other boxes but omits these), so diagnostics persist across user switches on shared devices.
Affected Component / Screen
Documents/errorsbox.hive, Documents/permissionsbox.hive; upload POST /diagnostics/errors
Evidence
errorsbox.hive held clear-text stack traces, source paths, device fingerprint, and session IDs that remained present after logout in the original build.
Proof of Concept
- Trigger any error, then read strings ~/Library/Containers/pro.emory.inspections.dev/Data/Documents/errorsbox.hive and observe clear-text stack traces, source paths, device fingerprint, session IDs.
- Log out and re-read; records remain.
Impact
Leaks internal application architecture and source paths and persists device/session metadata in clear text, surviving logout on shared devices. The exposure is app-internal metadata rather than credentials or customer PII.
Remediation
- Add errorsbox and permissionsbox to the logout/clear routine.
- Strip stack traces and source paths from release diagnostics; encrypt or purge after upload; consent-gate and TLS-pin the upload.
Retest
Retested build 756: FIXED. The shared clearInfo()/logout path was verified via Android dynamic proof (Hive delete-frames for jwt/refresh/auth-model/user-model and errors plus permissions boxes reduced to 0 bytes).
L001 · Insufficient Anti-Tampering (Missing Jailbreak Detection) - OWASP MASVS-RESILIENCE · CWE-693
No Jailbreak (Root) Detection
Description
The app performs no jailbreak detection - no checks for jailbreak artefacts (Cydia.app, cydia/Sileo URL schemes, /bin/bash, /usr/sbin/sshd, suspicious dyld images) or sandbox-integrity tests exist in the binary. As a result the app runs with full functionality on a jailbroken device, where the OS sandbox it relies on for the cleartext storage findings (F1-F3) no longer isolates its container and any process can read the token/PII files.
Affected Component / Screen
App.framework/App - no jailbreak-detection logic in the binary
Evidence
In the original build, strings of the binary returned no jailbreak-detection tokens and the app launched and operated normally with no warning or block.
Proof of Concept
- Inspect the binary: strings App.framework/App and grep for cydia, sileo, /bin/bash, /usr/sbin/sshd, jailbroken; no detection logic.
- Install and run the app on a jailbroken device; it launches and operates normally with no warning, restriction, or block.
Impact
On a jailbroken device the app runs unrestricted and the sandbox isolation that the cleartext Hive storage (F1-F3) depends on is void, so any process can read the token and customer-PII files. This is a defence-in-depth gap.
Remediation
- Add jailbreak detection (artefact, URL-scheme, sandbox-integrity, and dyld-image checks) and respond gracefully (warn, restrict sensitive features, signal the server).
- Do not rely on the sandbox alone for secrets; encrypt at rest and use the Keychain (see F2/F3) and re-validate authorization server-side.
Retest
Retested build 756: FIXED. Live on iOS the app shows a Device not supported block screen and refuses to run, with IOSSecuritySuite.framework bundled; build 758 additionally adds freeRASP/Talsec. This client-side RASP is bypassable on a real jailbroken device with Frida (inherent).
L002 · Improper Platform Usage (URL Scheme / Universal Link Misconfiguration) - OWASP MASVS-PLATFORM · CWE-939, CWE-919
Improper URL Handling: https Custom Scheme and Empty Universal-Link Declaration
Description
The app registers https (and mailto, tel) as custom URL schemes in CFBundleURLTypes with role Editor. Registering the https scheme as a custom app URL type is improper - custom-scheme handlers lack the domain-ownership verification that Universal Links provide. Simultaneously the Universal Links declaration is empty: com.apple.developer.associated-domains contains only applinks: with no domain, so no universal link is verified/owned. Handler analysis confirms the impact is latent: the app uses GetX routing with no deep-link plugin and no app code consumes an incoming URI token/redirect/next parameter. On iOS the https custom-scheme claim is additionally inert (iOS routes https to Universal Links/Safari), so it cannot intercept other apps traffic. The defect is the misconfiguration itself.
Affected Component / Screen
Info.plist CFBundleURLSchemes = [https, mailto, tel]; entitlement associated-domains = [applinks:] (empty domain)
Evidence
Info.plist listed https as a CFBundleURLSchemes entry and the associated-domains entitlement was applinks: with no domain.
Proof of Concept
- Inspect Info.plist: plutil -p Info.plist and grep CFBundleURLTypes; https is listed as a CFBundleURLSchemes entry.
- Inspect entitlements: codesign -d --entitlements shows associated-domains = [applinks:] with no domain.
Impact
Improper/ambiguous URL-scheme ownership and a non-functional Universal-Link declaration form a foundation for deep-link hijacking if an in-app handler is later added; no sensitive handler currently sits behind the schemes.
Remediation
- Remove https from CFBundleURLTypes; use Universal Links with a properly populated applinks:<domain> entitlement and a hosted apple-app-site-association file.
- Validate and bind any incoming-link tokens server-side (single-use, short-TTL).
Retest
Retested build 756: FIXED. CFBundleURLSchemes is now [mailto, tel] with https removed. Note associated-domains remains applinks: with no domain (Universal Links still unconfigured), a pre-existing item separate from this fix.
L003 · Information Exposure via Screen Capture / Backgrounding Snapshot - OWASP MASVS-PLATFORM · CWE-200
No Screenshot / App-Snapshot Protection
Description
The app implements no protection against the iOS app-switcher snapshot or screenshots of sensitive screens. When the app is backgrounded, iOS captures a snapshot of the current screen (stored on disk) for the multitasking UI; sensitive screens (login, inspection data, customer details, signatures) are not masked (e.g. via an overlay/blur on applicationWillResignActive), so the snapshot and any screenshot may expose sensitive content.
Affected Component / Screen
All sensitive screens - no snapshot masking / screenshot mitigation
Evidence
In the original build the backgrounding snapshot under Library/SplashBoard/Caches/Snapshots showed sensitive content unmasked and screenshots were unrestricted.
Proof of Concept
- Open a sensitive screen, background the app, and inspect the snapshot under Library/SplashBoard or Library/Caches/Snapshots on the device; sensitive content is visible. Screenshots are likewise unrestricted.
Impact
Sensitive on-screen PII and inspection data can leak via the multitasking snapshot stored on disk and via unrestricted screenshots, exploitable through device/backup access or shoulder-surfing.
Remediation
- On applicationWillResignActive/scenePhase not active, overlay a blur/placeholder over the key window so the snapshot does not capture sensitive content.
Retest
Retested build 756: FIXED (code confirmed; runtime needs a device). Runner.AppDelegate now has a privacyOverlay ivar plus applicationWillResignActive:/applicationDidBecomeActive: lifecycle hooks with addSubview:/removeFromSuperview, adding a privacy cover on background and removing it on foreground. Visual/forensic proof requires a physical device.
L004 · Excessive Permissions (Least Privilege / Privacy) - OWASP MASVS-PRIVACY · CWE-250, CWE-272
Over-Privileged Permissions (Always-On Location, Apple Music)
Description
The app requests NSLocationAlwaysAndWhenInUseUsageDescription (always location) where its stated purpose is only to geo-tag captured media (which needs only when-in-use location), and NSAppleMusicUsageDescription (Apple Music library access) which has no clear inspection-app purpose (likely pulled in by a video/audio plugin). These exceed least privilege and broaden the privacy footprint.
Affected Component / Screen
Info.plist NSLocationAlwaysAndWhenInUseUsageDescription, NSAppleMusicUsageDescription
Evidence
plutil of Info.plist showed both NSLocationAlwaysAndWhenInUse and NSAppleMusicUsageDescription present in the original build.
Proof of Concept
- Inspect Info.plist usage strings: plutil -p Info.plist and grep UsageDescription; NSLocationAlwaysAndWhenInUse and NSAppleMusicUsageDescription are present.
Impact
Unnecessary always-location and music-library access broadens the privacy footprint and creates compliance and App Review risk; there is no direct exploit.
Remediation
- Use NSLocationWhenInUseUsageDescription only (drop Always) for geo-tagging.
- Remove NSAppleMusicUsageDescription unless a feature genuinely needs the music library; identify and configure the contributing plugin.
Retest
Retested build 758: FIXED. Info.plist removed NSLocationAlwaysAndWhenInUseUsageDescription, NSLocationAlwaysUsageDescription and NSAppleMusicUsageDescription; only the minimal NSLocationWhenInUseUsageDescription remains. Build 756 still declared the Always plus AppleMusic strings, so this landed in 758.
L005 · Privacy - Insufficient Control over Personal Data Collection - OWASP MASVS-PRIVACY · CWE-359
Device Fingerprint and Identity Telemetry Without Clear Consent
Description
The app collects a device fingerprint (its own deviceId/_batchFingerprint plus the IDFV) and transmits diagnostics to the backend (POST /diagnostics/errors, /diagnostics/), and ships OneSignal (which collects a UserID) and Google telemetry pipelines (GoogleDataTransport/Clearcut/Phenotype). A user-reachable Diagnostics/Share-logs feature exists. For an EU-facing product these flows require explicit consent gating and processor documentation; the header-redaction allowlist in the diagnostics path does not run on the raw network logger (F1). The app does not call OneSignal.login (no external_id/email pushed) and no IDFA/ATT prompt is present, but the collection remains undeclared at the app level (F13).
Affected Component / Screen
Diagnostics upload POST /diagnostics/errors, /diagnostics/; OneSignal external_container_id; OneSignal App ID in bundled .env
Evidence
The Dart binary contained the diagnostics collectors and sinks (/diagnostics/errors, external_container_id) and diagnostics batches were observed on use.
Proof of Concept
- Confirm collectors/sinks in the Dart binary (/diagnostics/errors, external_container_id) and observe diagnostics batches on use.
Impact
Device fingerprint, session context, and device-to-user identity linkage were collected and transmitted without explicit consent controls, a GDPR concern for EU users. The exposure is compliance-oriented rather than a direct compromise.
Remediation
- Gate telemetry/diagnostics upload behind explicit, revocable consent (default off); minimise collected identifiers; document the OneSignal processor relationship.
Retest
Retested build 758: FIXED (code). The App binary now contains a diagnosticsConsent opt-in dialog (Title/Body/Allow/Decline) plus appTrackingTransparency. Runtime UI is not drivable on this Mac because the app RASP blocks, so judged on code plus Android parity; feature was WIP in 756.
L006 · Privacy Transparency / Compliance Gap - OWASP MASVS-PRIVACY · CWE-359
Missing App-Level Privacy Manifest (PrivacyInfo.xcprivacy) for Required-Reason APIs and Data Collection
Description
The app ships no PrivacyInfo.xcprivacy at the bundle root (and none in App.framework). The only privacy manifests present belong to third-party pods (OneSignal, SDWebImage, GoogleUtilities, etc.). Apple requires the app itself to declare an aggregate privacy manifest when it uses required-reason APIs - which it does via plugins (path_provider file-timestamp C617.1/0A2A.1, package_info_plus system-boot-time 35F9.1, UserDefaults CA92.1) - and when it collects data or contacts tracking domains. Because the app-level manifest is absent, the app declares no aggregate NSPrivacyTracking, no NSPrivacyTrackingDomains, and no NSPrivacyCollectedDataTypes of its own, despite collecting a user identifier and emitting Google telemetry. This is an App Store compliance gap and a MASVS-PRIVACY transparency failure.
Affected Component / Screen
Missing Runner.app/PrivacyInfo.xcprivacy (app-level aggregate manifest); required-reason APIs via path_provider, package_info_plus, OneSignal
Evidence
A test for Runner.app/PrivacyInfo.xcprivacy returned ABSENT and find of *.xcprivacy returned only third-party pod manifests in the original build.
Proof of Concept
- Run test -f on Runner.app/PrivacyInfo.xcprivacy; it returns ABSENT.
- Run find on Runner.app for *.xcprivacy; only third-party pod manifests appear, none at app root or in App.framework.
Impact
The app own data-collection and required-reason API usage were undeclared to Apple and to users, undermining privacy transparency and risking App Store rejection under the privacy-manifest mandate. The impact is compliance/transparency, not a direct exploit.
Remediation
- Add an app-level PrivacyInfo.xcprivacy declaring NSPrivacyAccessedAPITypes (with reason codes) and NSPrivacyCollectedDataTypes for the OneSignal/diagnostics identifiers, plus NSPrivacyTracking/NSPrivacyTrackingDomains (false/empty if no cross-app tracking).
Retest
Retested build 758: FIXED. Runner.app/PrivacyInfo.xcprivacy is now present at bundle root (3263 B) and substantive: it declares NSPrivacyAccessedAPITypes (FileTimestamp C617.1/0A2A.1, SystemBootTime 35F9.1, UserDefaults CA92.1, DiskSpace E174.1) and NSPrivacyCollectedDataTypes (DeviceID/CrashData, AppFunctionality, not tracking). Build 756 had only third-party SDK manifests.
L007 · Improper Build Configuration for Distribution - OWASP MASVS-CODE / MASVS-RESILIENCE · CWE-489, CWE-1188
Development-Flavor Build Distributed via TestFlight with Dart VM-Service (Observatory) Bonjour Remnant
Description
A development-flavor build (bundle id pro.emory.inspections.dev, active API_BASE_URL=https://dev-be.emory.pro) is signed and distributed via TestFlight with a production push entitlement (aps-environment=production), and the Info.plist carries a NSBonjourServices = [_dartobservatory._tcp] entry, the Dart VM Service (Observatory) discovery record, a non-release configuration remnant. At runtime this is inert (get-task-allow=false and the AOT snapshot is a release build, so the VM service is not exposed), but shipping a dev-backend build with debug-discovery metadata through a production distribution channel is a build-hygiene and dev-prod-separation defect.
Affected Component / Screen
Info.plist NSBonjourServices = [_dartobservatory._tcp]; dev bundle id plus API_BASE_URL=https://dev-be.emory.pro; TestFlight; aps-environment=production
Evidence
plutil of Info.plist showed NSBonjourServices _dartobservatory._tcp alongside the dev bundle id, dev API base URL, and TestFlight/aps-environment=production in the original build.
Proof of Concept
- plutil -p Info.plist and grep NSBonjourServices returns _dartobservatory._tcp.
- plutil -p Info.plist for CFBundleIdentifier shows inspections.dev; bundled .env shows API_BASE_URL=https://dev-be.emory.pro; codesign -d shows TestFlight distribution and aps-environment=production.
Impact
A dev-backend build with debug-discovery metadata was distributed through a production channel, weakening dev/prod separation and build hygiene; there is no direct exploit.
Remediation
- Build and distribute a dedicated release flavor (prod bundle id, prod backend) for TestFlight/App Store; strip the _dartobservatory._tcp NSBonjourServices entry and any debug discovery from release configurations.
Retest
Retested build 756: FIXED. NSBonjourServices is absent from Info.plist, removing the Dart Observatory Bonjour discovery record.
L008 · Improper Session Management / Missing Local Authentication - OWASP MASVS-AUTH · CWE-613, CWE-308
No App-Level Authentication Lock, Biometric Re-Authentication, or Session Inactivity Timeout
Description
The application provides no client-side protection of the authenticated session beyond the device passcode. There is no app-level lock, no biometric (Face ID / Touch ID) re-authentication gate, and no inactivity/idle timeout: the binary contains no LocalAuthentication/LAContext usage (no local_auth plugin linked), and the lifecycle handler (AppLifecycleManager) only persists state on backgrounding. Combined with the long-lived tokens (30-day access, 50-day refresh; F2), an unlocked-but-unattended device leaves a full Org-Admin session open indefinitely, and anyone who picks up the device operates the app as the logged-in user with no second factor. This is the runtime counterpart to the at-rest exposure in F2/F5.
Affected Component / Screen
utils/lifecycle_manager.dart (AppLifecycleManager) - no lock/blur/timeout; no LocalAuthentication/LAContext in App.framework
Evidence
In the original build the session stayed active after long idle/backgrounding with no re-auth, and strings of the binary returned no LAContext/LocalAuthentication/biometric matches.
Proof of Concept
- Log in, then leave the app idle or backgrounded for an extended period and reopen it; the session is still active with no re-authentication prompt.
- Inspect the binary: strings App.framework/App and grep for LAContext, LocalAuthentication, evaluatePolicy, biometric; no matches.
- Confirm no idle-timeout: AppLifecycleManager contains no timer-driven logout, and the 30/50-day tokens remain valid until manual logout.
Impact
A momentarily-unattended unlocked device grants an attacker full Org-Admin operation inside the app; sessions never expire client-side, so a stale session on a shared or lost device stays usable for the full token lifetime, and no biometric/app-lock option protects a high-value B2B session handling customer PII.
Remediation
- Add an app-lock with biometric/passcode re-authentication on launch and on return-to-foreground (e.g. local_auth with LAContext), with a short configurable grace period.
- Enforce a client-side inactivity timeout that clears the in-memory session and requires re-authentication.
- Pair with shorter server-side token lifetimes and a token-type claim (see F2) so a recovered session is bounded.
Retest
Retested build 756: FIXED (code). The 756 binary contains _AppLockGateState, _PasscodeSetupScreenState, _PasscodeVerifyScreenState, and app_passcode_hash/app_passcode_salt, implementing an app-lock/passcode gate. The UI flow was not drivable because the app RASP blocks on this Mac.
L009 · Improper Input Validation / Insecure Storage of Configuration - OWASP MASVS-CODE / MASVS-STORAGE · CWE-20, CWE-312
Printer Destination IP Stored in Cleartext and Insufficiently Validated
Description
The label-printing feature lets the user enter a printer IP address that the app stores in the unencrypted appSettingsHiveBox and then connects to over the local network (via the Brother SDK platform channel PrinterChannel.printImage/printBatch) to print inspection labels and QR/PDF images. The value is validated only by a permissive [0-9.] character-class regex (printer_ip_screen.dart) - there is no octet-range, structure, or reserved/loopback check - so any digit-and-dot string is accepted as a network destination. The host is reached in clear text on the LAN, and the configured IP persists in plaintext storage alongside the app other Hive boxes.
Affected Component / Screen
printer_ip_screen.dart / printer_ip_service.dart (validation [0-9.]); utils/printer_channel.dart; Documents/appSettingsHiveBox.hive (key ipAddress)
Evidence
Settings accepted an arbitrary value such as 0.0.0.0 (only [0-9.] enforced) and stored it in cleartext in appSettingsHiveBox.hive under key ipAddress in the original build.
Proof of Concept
- Open Settings then Printer, enter an arbitrary value such as 0.0.0.0; observe it is accepted (only [0-9.] enforced) and saved.
- Read the stored value in clear: strings ~/Library/Containers/pro.emory.inspections.dev/Data/Documents/appSettingsHiveBox.hive and grep ipAddress.
- Trigger a test print; the app opens a cleartext LAN connection to the supplied host.
Impact
An attacker or a mistaken user can point printing at an arbitrary LAN host, sending label/QR content to an unintended device, and the destination IP is retained in plaintext application storage. No credentials are involved.
Remediation
- Validate the IP with a strict IPv4 (octet-range) parser; reject malformed, loopback, and out-of-subnet addresses.
- Store printer configuration in the encrypted / file-protected store recommended for the app other data (see F3).
- Where the printer supports it, prefer an authenticated/encrypted print path over cleartext LAN printing.
Retest
Retested build 756: FIXED (code). The 756 binary contains an isValidPrinterIp function and a printerInvalidIp locale string, adding proper printer-IP validation.
L010 · Insufficient Anti-Tampering (Missing Anti-Debugging / Anti-Instrumentation) - OWASP MASVS-RESILIENCE · CWE-693, CWE-489
No Debugger or Anti-Instrumentation (Anti-Tampering) Detection
Description
The app implements no anti-debugging or anti-instrumentation/tamper detection - no ptrace(PT_DENY_ATTACH), no sysctl P_TRACED check, no Frida/objection/hook detection, and no runtime integrity/checksum self-check exist in the binary. iOS provides baseline protection (the build is Apple-signed and get-task-allow=false, so a standard debugger cannot attach to a release build), but the app does not detect or resist a debugger or dynamic-instrumentation framework on a jailbroken/instrumented device, so token handling and client-side controls can be hooked and modified at runtime with no resistance.
Affected Component / Screen
App.framework/App - no anti-debug / anti-instrumentation / runtime-integrity (RASP) logic
Evidence
In the original build, strings of the binary returned no ptrace/P_TRACED/PT_DENY_ATTACH/frida/objection matches and Frida/objection attached and hooked with no detection or termination.
Proof of Concept
- Inspect the binary: strings App.framework/App and grep for ptrace, P_TRACED, PT_DENY_ATTACH, frida, gum-js, objection; no anti-debug/anti-instrumentation logic.
- On a jailbroken device, attach Frida/objection and hook a token-handling or Dart runtime function; no detection and no termination, the hooks apply unimpeded.
Impact
A debugger or instrumentation framework could hook and modify token handling and client-side controls at runtime with no resistance, enabling bypass of client checks and observation of in-memory secrets; a defence-in-depth gap.
Remediation
- Add anti-debugging (ptrace PT_DENY_ATTACH, sysctl P_TRACED checks) and anti-instrumentation/hook detection, with graceful, server-corroborated responses.
- Add a runtime integrity self-check and re-validate authorization server-side; consider Apple App Attest / DeviceCheck so the backend can reject tampered or instrumented clients.
Retest
Retested build 758: FIXED. Build 758 bundles TalsecRuntime.framework and freerasp.framework (freeRASP), adding debugger/instrumentation/integrity detection with onMalwareDetected channels, alongside the pre-existing IOSSecuritySuite jailbreak block. In 756 this anti-instrumentation work had been skipped by decision. Client-side RASP is bypassable on a real jailbroken device (inherent, LOW).
L011 · Insufficient Anti-Reversing (Reliance on Security Through Obscurity) - OWASP MASVS-RESILIENCE · CWE-656
No Dart Code Obfuscation (Application Logic Recoverable)
Description
The Dart code is not obfuscated: the App.framework/App Dart binary ships with original class, method, and field symbols recoverable (e.g. setJwtToken, shared-box, the storage key names, endpoint paths, and the full decompiled package tree under emory_mobile_app/...), and the bundled flutter_assets (including .env) are in clear. While the main Runner Mach-O is FairPlay-encrypted, the Flutter App.framework is not, so the entire application logic - storage key names, API structure, token handling, and the on-device ML pipeline - can be reverse-engineered directly from a pulled bundle, with no jailbreak. This lowers the cost of crafting the storage/token attacks in F1-F3 and the API-abuse paths.
Affected Component / Screen
App.framework/App - unencrypted, unobfuscated Dart (original symbols recoverable)
Evidence
In the original/756 build, strings of App.framework/App returned application symbols and storage keys (setJwtToken, shared-box, jwt-token, emory-refresh-token) in clear, confirming no Dart obfuscation.
Proof of Concept
- Pull the app bundle and inspect: strings App.framework/App and grep for setJwtToken, shared-box, jwt-token, emory-refresh-token; application symbols and storage keys return in clear.
- Decompile the AOT snapshot (e.g. with Blutter); the full emory_mobile_app package tree (API, storage, middle/presentation layers) is recoverable.
Impact
The complete application logic, storage key names, and API structure were recoverable without a jailbreak using standard tooling, lowering the cost of building the storage/token (F1-F3) and API-abuse attacks; no direct compromise on its own.
Remediation
- Build with flutter build ipa --obfuscate --split-debug-info=<dir> so Dart symbols are stripped/renamed; retain the debug-info mapping privately for crash symbolication.
- Avoid bundling environment files (.env) and other readable config in flutter_assets (see F15).
Retest
Retested build 758: FIXED. The App binary now has 0 package:emory_mobile_app URIs and 0 app class names (InspectionModel/Redaction/SecureBoxKey/HiveAesCipher), versus 216 package URIs plus readable app classes in the 756 unobfuscated control. The binary is still readable (Hive strings present), so the 0 reflects obfuscation, not encryption.
I001 · Information Disclosure (Build Configuration) - OWASP MASVS-STORAGE · CWE-200
Backend Topology Disclosed in Bundled .env Asset
Description
The Flutter bundle ships App.framework/flutter_assets/.env in clear text, disclosing the backend topology - the active dev host, a commented-out production host (prod-be.emory.pro), internal/local development IPs, and Render staging hosts - plus the OneSignal app id. No private keys are present, but it maps the environment and confirms the iOS/Android/web clients share the same backend.
Affected Component / Screen
App.framework/flutter_assets/.env
Evidence
cat of App.framework/flutter_assets/.env showed the active dev API base URL plus commented prod/Render/local hosts and the OneSignal app id in the original build.
Proof of Concept
- cat App.framework/flutter_assets/.env shows the active API_BASE_URL=https://dev-be.emory.pro, commented prod/Render/local hosts, and ONE_SIGNAL_APP_ID.
Impact
Reveals production/dev/internal host topology, aiding targeting of other environments; reconnaissance value only, with no secret material (the OneSignal app id is a public identifier).
Remediation
- Ship only the single target environment base URL per build flavor; inject config at build time rather than bundling a multi-environment .env.
Retest
Retested build 758: FIXED. The bundled .env is now 139 B and contains only APP_NAME, API_BASE_URL=https://dev-be.emory.pro, BUILD_DATE, and one OneSignal id. Build 756 still leaked prod-be, internal IPs, Render hosts, and localhost.
/ Sign-off
Reviewers & Signatories
- ShashankCo-founder, CredShields
Shashank
Co-founder & CEO · CredShields
Security analyst and penetration tester with 12 years of experience in pentesting and 5 years in managing bug bounty programs. Listed in Google, Facebook, Apple, and 30+ websites' hall of fame for reporting valid security issues.
- CVE-2017-7063
- CVE-2017-7062
- CVE-2017-2458
Aditya Dixit
Audit & Research Lead · CredShields
Senior security researcher and penetration tester with 10+ years in cybersecurity. Led pentest teams at Cobalt and HackerOne; previously at Binary.com. Executed 500+ pentests across banking, fintech, payments, healthcare, telecom, and media, covering web, mobile (iOS/Android), network, cloud, code review, and smart contracts.
- OSCP
- CREST CPSA
- AWS Security Specialty
- AWS Cloud Practitioner
- Qualys VMDR