AI speed. Human accountability.
CredShields · Mobile Application Penetration Test
Android 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 Android application. Findings were identified and verified by the CredShields security team.
- Critical1
- High2
- Medium4
- Low10
- Info1
- Client
- Emory Pro
- Scope
- Emory Pro Android application
- Lead
- Shashank (shashank@credshields.com)
/ Contents
Table of Contents
- 01Executive Summary
- 02Vulnerability Summary
- 03Detailed Findings
- C001Cleartext Account Password, Session Tokens, and Organisation-Wide PII Persisted in Local Network LogsCritical
- H001Long-Lived JWT Access and Refresh Tokens Stored Unencrypted in HiveHigh
- H002Customer PII, Signatures, and Inspection Photos Cached Unencrypted On-DeviceHigh
- M001No Certificate Pinning and Cleartext Traffic Permitted App-WideMedium
- M002Unrestricted Backup of the Application SandboxMedium
- M003Development Build Wired to the Production Firebase ProjectMedium
- M004Broken Authorization in the Push-Notification Subsystem (Token BOLA + Unrestricted Send)Medium
- L001Missing Root DetectionLow
- L002Missing Emulator DetectionLow
- L003Missing Debugger Detection and Device-Integrity AttestationLow
- L004No Code Obfuscation (Dart Symbols Recoverable)Low
- L005Plaintext Diagnostics Stored and Retained After LogoutLow
- L006Password-Reset App Link Fails Verification and Is Deep-Link HijackableLow
- L007Missing Screenshot / Task-Switcher Protection (FLAG_SECURE Absent)Low
- L008Unsanitised Provider Filename Used in Cache Path (File-Picker Path Traversal)Low
- L009Over-Privileged READ_PHONE_STATE PermissionLow
- L010Device Fingerprint and Identity Telemetry Without Clear Consent ControlsLow
- 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 Salico GmbH on the Emory Pro Android Application. The assessment identified a total of 18 security findings, comprising 1 Critical, 2 High, 4 Medium, 10 Low, and 1 Informational vulnerability.
The most severe finding, rated Critical, was the persistence of Cleartext Account Passwords, Session Tokens, and Organisation-Wide PII in Local Network Logs. Additionally, two High severity findings were identified: Long-Lived JWT Access and Refresh Tokens Stored Unencrypted, and Customer PII, Signatures, and Inspection Photos Cached Unencrypted On-Device. These critical and high findings represent significant risks related to sensitive data exposure and should be prioritized for remediation.
Following a retest, it was confirmed that 17 of the 18 identified findings have been successfully fixed, with the remaining 1 finding acknowledged by Salico GmbH.
Assessment overview
This engagement comprised a comprehensive penetration test of the Emory Pro Android Application for Salico GmbH. The review approach focused on identifying security vulnerabilities across various layers of the mobile application stack. This included static analysis of the application's source code and compiled binaries, dynamic analysis during runtime to observe its behavior and interactions, and API testing to assess the security of backend communications. Specific attention was paid to client-side data storage, network communication security, authentication and authorization mechanisms, and potential misconfigurations related to the Android operating system and third-party dependencies.
The assessment identified a total of 18 findings, encompassing one Critical, two High, four Medium, ten Low, and one Informational severity issue. CredShields is pleased to report that, following the retest, 17 of these findings have been successfully fixed by the Salico GmbH development team, demonstrating a strong commitment to security. One remaining issue has been acknowledged by the client.
The most critical finding, C001: Cleartext Account Password, Session Tokens, and Organisation-Wide PII Persisted in Local Network Logs, highlighted a severe risk of sensitive data exposure through insecure logging practices. This was complemented by High severity findings related to H001: Long-Lived JWT Access and Refresh Tokens Stored Unencrypted in Hive and H002: Customer PII, Signatures, and Inspection Photos Cached Unencrypted On-Device, which collectively pointed to pervasive insecure data storage practices on the device. These issues presented significant risks for unauthorized access to sensitive user and organizational data.
Medium severity findings identified other areas for improvement. These included M001: No Certificate Pinning and Cleartext Traffic Permitted App-Wide, which exposed network communications to man-in-the-middle attacks, and M002: Unrestricted Backup of the Application Sandbox, which could lead to data leakage through platform backup features. Furthermore, M003: Development Build Wired to the Production Firebase Project indicated a configuration boundary violation that could introduce risks, while M004: Broken Authorization in the Push-Notification Subsystem presented a potential for unauthorized push notification distribution.
The Low and Informational findings addressed a range of issues, primarily related to anti-tampering and resiliency, such as L001: Missing Root Detection, L002: Missing Emulator Detection, L003: Missing Debugger Detection, and L004: No Code Obfuscation, which make the application more susceptible to reverse engineering and modification. Other low-severity findings highlighted minor data exposure risks like L005: Plaintext Diagnostics Stored and Retained After Logout, platform-specific vulnerabilities such as L006: Password-Reset App Link Fails Verification and Is Deep-Link Hijackable and L007: Missing Screenshot / Task-Switcher Protection, and privacy concerns like L009: Over-Privileged READPHONESTATE Permission and L010: Device Fingerprint and Identity Telemetry Without Clear Consent Controls. The Informational finding, I001: Backend Topology Disclosed in Bundled .env Asset, provided insights into the application's infrastructure that could aid an attacker. The diligent efforts of the Salico GmbH team in addressing the vast majority of these findings are commendable, significantly enhancing the security posture of the Emory Pro Android Application.
Audited scope
Emory Pro (Dev) - pro.emory.inspections.dev
Retested scope
Emory Pro (Dev) - pro.emory.inspections.dev
Engagement details
- Client
- Emory Pro
- Assessment type
- Grey-box Assessment
- Method
- Black-box testing
- Engagement
- Emory Pro Mobile (Android) Security Assessment
- Report date
- July 1, 2026
- Start date
- June 5, 2026
- End date
- June 9, 2026
- Retest date
- July 1, 2026
- Total findings
- 18
- 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
18 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-Wide PII Persisted in Local Network Logs
Description
The Android client writes every API request and response — including the login request body — verbatim into an unencrypted Hive log database (logsHiveBox → app_flutter/logshivebox.hive). The network logger performs no redaction on bodies. As a result the user's plaintext account password, both JWT session tokens, and the full /user/organization roster (every colleague's email, employee ID and role) are stored on the device in clear text. Anyone with the device, a backup, or transient root can read live credentials and the whole organisation's PII with a single strings command.
Affected Component / Screen
app_flutter/logshivebox.hive; logging.dart → logMessage; POST /user/authenticate, GET /user/organization
Evidence
logshivebox.hive contained the cleartext login password, both JWTs, and the full org roster; recovered with a single strings command.
Proof of Concept
- Install the app and log in once as any user (the real password is recorded verbatim).
- On a rooted device (or after unpacking an
adb backup), read the log box:
adb shell "su 0 cat /data/data/pro.emory.inspections.dev/app_flutter/logshivebox.hive" | strings | grep -i password - Observe the login request body in clear text, including
password: <the user's real password>, immediately followed by the auth response containing both JWTs. - Scroll the same file to the
GET /user/organizationresponses listing every user's email, employee_id and role.
Observed evidence (password masked):
{requestUrl: /user/authenticate, body: {email: emoryadmin…, password: ********}, response: {token: eyJ…(access), refreshToken: eyJ…(refresh)}}
{requestUrl: /user/organization, response: [{id:196, email: emoryadmin…, user_role_key: OA}, {id:197, … employee_id: AGENT01, user_role_key: EFA}, …]}
Impact
Theft of a device, cloud/USB backup, or transient root yields the victim's reusable password plus live session tokens — full account takeover that survives token expiry. It also leaks the complete organisation user directory (emails, employee IDs, roles), enabling targeted phishing and lateral attacks. Storing credentials and third-party PII in clear text is a direct confidentiality/GDPR violation for an EU-facing product.
Remediation
- Never log request/response bodies for authentication endpoints, and never log the
passwordfield — exclude/user/authenticateand/user/renew, and droppassword/token/refreshTokenkeys before persisting. - Apply the existing diagnostics redaction allowlist to the
logsHiveBoxnetwork logger. - Gate all network logging behind
kReleaseMode == falseso release builds never persist bodies. - If logs must persist, encrypt the box with a KeyStore-backed
HiveAesCipherkey and cap retention.
Retest
Retested against build 758: FIXED. A shared redaction layer now masks auth headers, passwords, tokens and emails before persistence; logshivebox.hive is encrypted (entropy 8.00) with zero plaintext password/JWT/email across all boxes.
H001 · Insecure Data Storage (Cleartext Storage of Credentials) — OWASP MASVS-STORAGE · CWE-312, CWE-522, CWE-922
Long-Lived JWT Access and Refresh Tokens Stored Unencrypted in Hive
Description
The app persists the JWT access token (30-day lifetime) and refresh token (50-day lifetime) in an unencrypted Hive box (shared-box) under keys jwt-token and emory-refresh-token, alongside an auth-model record. No flutter_secure_storage, Android KeyStore, or HiveAesCipher is used anywhere in the binary, so tokens are stored in clear text. Because the backend accepts the refresh token directly as an access bearer and lifetimes are measured in weeks, a single extraction yields up to a 50-day account takeover.
Affected Component / Screen
app_flutter/shared-box.hive (keys jwt-token, emory-refresh-token, auth-model); auth_info.dart setJwtToken/setRefreshToken (plain box, no cipher)
Evidence
Two clear-text JWTs (30/50-day exp) recovered from shared-box.hive and replayed as Bearer tokens; also recovered live from process memory via Frida.
Proof of Concept
- Install and log in on a rooted device (or unpack an adb backup).
- Read shared-box.hive with su and strings, then grep for the eyJ JWT pattern.
- Decode the two JWT payloads; confirm role, user_id, organization_id and exp 30/50 days out.
- Replay either token as Authorization: Bearer <token> against https://dev-be.emory.pro — the refresh token works directly as an access bearer.
- Tokens are also recoverable from process memory via Frida (Memory.scanSync for the JWT header).
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
Theft of a device, a backup, or transient root yields a long-lived organisation-admin session with no re-authentication. Combined with the server-side refresh-token-as-access-token weakness, the usable window extends to ~50 days. No KeyStore binding means even a screen-locked device exposes the tokens to forensic extraction.
Remediation
- Move tokens to flutter_secure_storage (AndroidKeyStore-backed).
- If Hive is retained, open the box with a KeyStore-derived AES key via HiveAesCipher.
- Shorten server-side token lifetimes (access <= 15 min; refresh <= 7 days with rotation) and add a token-type claim so a refresh token cannot be replayed as an access bearer.
- Ensure logout deletes all token keys.
Retest
Retested build 758: FIXED. shared-box is now opened with a HiveAesCipher keyed from a Keystore/Keychain-backed AES key; token values are ciphertext at rest (box entropy 7.99) and logout deletes jwt-token, emory-refresh-token and auth-model.
H002 · Insecure Data Storage (Cleartext Storage of Sensitive Information) — OWASP MASVS-STORAGE · CWE-312, CWE-311
Customer PII, Signatures, and Inspection Photos Cached Unencrypted On-Device
Description
The app caches business data in unencrypted Hive boxes and plain files. Every box is opened with Hive.openBox(...) with no encryptionCipher and no Android KeyStore-backed key anywhere. As a result customer PII (names, emails, company names), assignee handwritten-signature data, inspection records, and the inspection photographs/videos are stored in clear text and are recoverable from any rooted device, backup, or forensic image.
Affected Component / Screen
app_flutter/apidatahivebox.hive, customers-box.hive, shared-box.hive; app_flutter/image, video, marking, profile (raw media); boxes opened via Hive.openBox with no encryptionCipher
Evidence
customers-box.hive and apidatahivebox.hive yielded clear-text customer emails, company names, signature data and S3 report URLs; app_flutter/image held raw inspection JPGs.
Proof of Concept
- Log in and load inspections/customers so the caches populate.
- On a rooted device, dump customers-box.hive, apidatahivebox.hive and shared-box.hive with su and strings.
- Observe clear-text customer emails, company names, assignee signature data and S3 object URLs.
- List app_flutter/image and pull the unencrypted inspection photos.
Impact
Discloses customer and employee PII and signature data to anyone with device/backup access. Inspection photographs and PDF report URLs (potential further PII / evidentiary content) are exposed. Confidentiality/GDPR exposure for an EU-facing inspection product.
Remediation
- Open all Hive boxes holding PII with a KeyStore-backed cipher (HiveAesCipher).
- Store cached media in an encrypted container or file-level-encrypted app-private storage; purge on logout.
- Add a data-retention policy and a wipe-local-data action; clear image/video/marking/profile on logout.
- Set allowBackup=false or exclude these paths.
Retest
Retested build 758: PARTIAL. The five PII-bearing boxes are now encrypted (HiveAesCipher; entropy 7.88–8.00). Per-file media (inspection photos/signatures written to the device gallery as crash-backup) remains plaintext — the client has formally accepted this residual on the basis of company-managed devices and company cloud accounts.
M001 · Insufficient Transport Layer Protection — OWASP MASVS-NETWORK · CWE-295, CWE-319
No Certificate Pinning and Cleartext Traffic Permitted App-Wide
Description
The app performs no certificate pinning at any layer and permits cleartext traffic process-wide (usesCleartextTraffic=true, no networkSecurityConfig). The dio HTTP stack is created with default options (no badCertificateCallback, no custom SecurityContext, no pinning) and followRedirects:true. A network attacker who can present a trusted certificate (rogue/compromised CA, user-installed CA, corporate TLS inspection) can intercept the bearer JWT and refresh token; the cleartext allowance plus automatic redirect-following also permit a downgrade to HTTP.
Affected Component / Screen
AndroidManifest usesCleartextTraffic=true, no networkSecurityConfig; dio _createDio (followRedirects:true, no cert override); bundled .env retains commented http:// endpoints
Evidence
Manifest permitted cleartext app-wide and no pinning was present in dio; a proxy CA intercepted the bearer token with no rejection.
Impact
Credential and PII interception under MITM (rogue Wi-Fi, over-trusting/compromised CA, corporate inspection). The cleartext allowance plus followRedirects:true enables a downgrade to plain HTTP if a malicious or misconfigured host returns a redirect.
Remediation
- Set usesCleartextTraffic=false and add a networkSecurityConfig that disallows cleartext and pins the backend SPKI.
- Add app-layer pinning in dio (validate the leaf/intermediate SPKI hash) so pinning survives system-trust-store tampering.
- Restrict redirect-following or validate redirects stay on the same https origin.
- Remove cleartext host entries from the bundled .env.
Retest
Retested build 758: FIXED. Manifest sets usesCleartextTraffic=false and a strict networkSecurityConfig blocks cleartext app-wide (no domain exceptions). Certificate pinning remains intentionally skipped (acknowledged by the client).
M002 · Insecure Data Storage (Backup Misconfiguration) — OWASP MASVS-STORAGE · CWE-530, CWE-312
Unrestricted Backup of the Application Sandbox
Description
AndroidManifest sets allowBackup=true and declares neither fullBackupContent nor dataExtractionRules. The entire app sandbox — including the clear-text token box, the credential-bearing network log box, the diagnostics box, and the cached customer PII/media — is therefore eligible for adb backup and Auto Backup to the user Google account, allowing data extraction without root.
Affected Component / Screen
AndroidManifest allowBackup=true (no dataExtractionRules/fullBackupContent); backed-up paths include shared-box.hive, logshivebox.hive, errorsbox.hive, customers-box.hive, apidatahivebox.hive, app_flutter/image
Evidence
allowBackup=true with no extraction rules; adb backup captured the cleartext boxes without root.
Proof of Concept
- Enable USB debugging and connect over ADB.
- Run adb backup -f emory.ab pro.emory.inspections.dev and approve the on-device prompt.
- Unpack the backup and locate app_flutter/logshivebox.hive and shared-box.hive.
- Recover the clear-text password and JWTs from the unpacked boxes.
Impact
Provides a credential/PII theft path that does not require root, broadening the attacker population to anyone with brief physical access or the victim Google account. The backup carries every weakness of F1–F3 into an exportable archive.
Remediation
- Set allowBackup=false if backups are not required.
- If backup is desired, add dataExtractionRules (Android 12+) and fullBackupContent that exclude the sensitive boxes.
- Moving tokens/credentials to KeyStore-backed storage and not logging bodies (F1, F2) makes the backup non-sensitive regardless.
Retest
Retested build 758: FIXED. Manifest sets allowBackup=false; adb backup produces no app data and run-as is denied.
M003 · Environment / Configuration Boundary Violation — OWASP MASVS-CODE · CWE-1188, CWE-668
Development Build Wired to the Production Firebase Project
Description
The development application ([DEV] Emory Pro) embeds the production Firebase configuration: project emory-pro-prod, the production storage bucket, prod app id and Google API key. A non-production build referencing production Firebase services breaks environment isolation: dev/test push tokens, crash data and any future Storage/Firestore access are co-mingled with production, and the prod configuration is shipped to whoever holds the dev artifact. The embedded API key is currently not abusable (Maps disabled, Auth CONFIGURATION_NOT_FOUND, Firestore/Storage disabled), so this is a governance/boundary issue rather than a live exposure — but it becomes exploitable the moment any of those services is enabled with permissive rules.
Affected Component / Screen
res/values/strings.xml: project_id=emory-pro-prod, google_storage_bucket=emory-pro-prod.firebasestorage.app, google_app_id=1:274624387215:android:5abd..., google_api_key=AIza..., gcm_defaultSenderId=274624387215
Evidence
Dev build resources referenced project_id=emory-pro-prod and the production storage bucket.
Proof of Concept
- Inspect the artifact: aapt dump --values resources base.apk and grep for project_id/storage_bucket/google_app_id.
- Observe project_id=emory-pro-prod and the prod storage bucket in a .dev build.
- Confirm (read-only) current non-abusability: Auth signUp -> CONFIGURATION_NOT_FOUND, Firestore read -> 403 (API disabled), Storage -> 404.
Impact
Development/test activity touches the production Firebase project (push registration, crash reporting, any Storage/Firestore use), undermining environment separation. The production Firebase configuration is distributed inside a dev build; if Firestore/Storage/Auth are later enabled with default-open rules, the embedded config provides a ready foothold.
Remediation
- Use a separate Firebase project for dev/staging and inject the matching google-services.json per build flavor; never ship prod config in non-prod builds.
- Restrict the Android API key to the app package + signing certificate and to only the Firebase APIs actually used (FCM).
- Define explicit least-privilege Firebase Security Rules for Firestore/Storage before enabling those services.
Retest
Retested build 758: FIXED. The 758 build embeds a separate dev Firebase project (project_id=emory-pro-dev, sender 25881567380, storage emory-pro-dev.firebasestorage.app); the production project config is no longer shipped in the dev artifact.
M004 · Broken Object/Function Level Authorization — OWASP API1/API5/API3 · CWE-639, CWE-915, CWE-285
Broken Authorization in the Push-Notification Subsystem (Token BOLA + Unrestricted Send)
Description
The push-notification API endpoints enforce no authorization. update-firebase-token trusts a client-supplied user_id, so any authenticated account — including the lowest-trust read-only Customer — can register an attacker-controlled device token under any other user account (no ownership or existence check). Because the backend pushes a user notifications to every registered token, an attacker who registers their device under a victim user_id receives the victim push notifications and can pollute routing. Separately, send-custom-notification has no function-level authorization — any role can send arbitrary push notifications through the org OneSignal integration. update-firebase-token-status additionally returns raw MySQL driver result metadata. These endpoints are exercised only by the mobile client and are outside the web/API report scope.
Affected Component / Screen
POST /notification/update-firebase-token (accepts client user_id — BOLA/mass assignment); POST /notification/update-firebase-token-status (leaks raw SQL result); POST /notification/send-custom-notification (callable by any role — BFLA)
Evidence
A read-only Customer registered a device token to the Org-Admin user_id and to a nonexistent id (both 200); any role could call send-custom-notification (200); the status endpoint leaked MySQL result metadata.
Proof of Concept
- Authenticate as any low-privilege account (e.g. read-only Customer).
- POST /notification/update-firebase-token with {token:<attacker-device>,user_id:196} (196 = Org Admin) -> 200 New token inserted.
- Repeat with user_id:99999 (nonexistent) -> still 200, confirming no ownership/existence check.
- POST /notification/send-custom-notification as the same account -> 200 with the OneSignal send result, confirming any role can dispatch push.
HTTP Request
POST /notification/update-firebase-token HTTP/2
Host: dev-be.emory.pro
Authorization: Bearer <ANY_LOW_PRIV_JWT>
Content-Type: application/json
source: web
{token:attacker-device-token,user_id:196}Impact
An attacker can intercept another user (including an admin) push notifications, spoof/phish branded push notifications to known devices via the org OneSignal app, and corrupt notification routing with junk tokens. Raw SQL result leakage aids backend fingerprinting.
Remediation
- Derive user_id server-side from the authenticated token on update-firebase-token/-status; never accept it from the body.
- Add function-level authorization on send-custom-notification (restrict to a server-side role/capability).
- Validate that referenced users exist and belong to the caller organisation.
- Return generic success/error bodies; never surface raw database driver result objects.
Retest
Retested live on dev-be (2026-06-30): FIXED. update-firebase-token BOLA is fixed (server derives the user from the JWT and ignores any client user_id — a nonexistent id returns the same success). send-custom-notification is now role-gated (OA admin 200; EFA and read-only Customer 403; unauthenticated 401). update-firebase-token-status now returns a clean success body with no raw SQL result.
L001 · Insufficient Anti-Tampering — Missing Root Detection (OWASP MASVS-RESILIENCE) · CWE-919, CWE-693
Missing Root Detection
Description
The app does not detect whether it is running on a rooted device. No root-detection logic (RootBeer, su/Magisk binary checks, mount/property inspection) is present in the Dart, native, or Java layers. Root grants an attacker unrestricted read/write access to the app sandbox — which is what enables extraction of the cleartext credentials, tokens and PII in F1–F3 and F6 — and a foundation for runtime instrumentation. The app launched and operated normally on a fully rooted device with no warning.
Affected Component / Screen
Whole application — no root-detection check in libapp.so (Dart) or the native/Java shim
Evidence
App ran unrestricted on a rooted emulator; no root-detection symbols present in libapp.so.
Proof of Concept
- Install and run the app on a rooted device/emulator (su available) — it launches and functions normally with no warning.
- Confirm no root-detection logic: strings libapp.so grep for rootbeer/magisk/su-paths returns nothing app-specific.
Impact
A rooted device — the prerequisite for the cleartext-storage extraction in F1–F3/F6 — is neither detected nor mitigated, and it provides an unrestricted base for tampering and instrumentation.
Remediation
- Add root detection (RootBeer-style: su binaries, Magisk, dangerous props, writable system paths).
- On detection, degrade gracefully and report the signal server-side; never rely on the client decision alone.
Retest
Retested build 758: FIXED. The app now integrates flutter_jailbreak_detection plus a hard root/jailbreak block at startup (root_guard.dart) and freeRASP/Talsec; on a rooted device the app blocks before login. (Client-side RASP is bypassable on a real device — inherent.)
L002 · Insufficient Anti-Tampering — Missing Emulator Detection (OWASP MASVS-RESILIENCE) · CWE-919, CWE-693
Missing Emulator Detection
Description
The app does not detect execution in an emulator or virtualised environment (no checks for QEMU/goldfish/ranchu, ro.kernel.qemu, generic build props, absent sensors/telephony). Emulators lower the cost of automated/bulk abuse, dynamic analysis and reverse engineering; the entire static assessment ran against the app in a standard Android emulator with no impediment.
Affected Component / Screen
Whole application — no emulator-detection check in libapp.so or the native/Java shim
Evidence
App ran unrestricted in a standard emulator; no emulator-detection symbols present.
Proof of Concept
- Run the app in a standard Android emulator — it launches and operates normally.
- Confirm no emulator-detection logic: strings libapp.so grep for qemu/goldfish/ranchu returns only engine diagnostics.
Impact
Automated/bulk abuse, dynamic analysis and reverse engineering can be carried out in a disposable emulator with no friction.
Remediation
- Add emulator/virtual-environment detection (build-prop and device-node heuristics, sensor/telephony presence, hypervisor indicators).
- Combine with root (F9) and debugger (F11) signals and evaluate server-side via Play Integrity.
Retest
Retested build 758: FIXED. freeRASP/Talsec is now integrated (libts.so + FreeraspPlugin) with a simulator threat check; the app hard-terminates on an emulator.
L003 · Insufficient Anti-Tampering — Missing Anti-Debug / Integrity Attestation (OWASP MASVS-RESILIENCE) · CWE-693, CWE-919
Missing Debugger Detection and Device-Integrity Attestation
Description
The app performs no anti-debugging and no device/app-integrity attestation. There is no check for an attached debugger or dynamic-instrumentation framework (Debug.isDebuggerConnected, ptrace/TracerPid, Frida/Xposed artefacts) and no Play Integrity/SafetyNet attestation. An attacker can attach frida/objection and hook any function — token storage, authorization gates, network handling — with no resistance, and the backend has no signal that a request originates from a genuine, untampered app.
Affected Component / Screen
Whole application — no anti-debug and no attestation call in libapp.so or the native/Java shim
Evidence
objection attached and hooked the running app with no anti-debug resistance; no anti-debug/attestation symbols were present.
Proof of Concept
- Start frida-server and run objection -g pro.emory.inspections.dev explore — it attaches with no resistance.
- Confirm no anti-debug/attestation logic: strings libapp.so grep for ptrace/TracerPid/frida/SafetyNet/PlayIntegrity returns only engine diagnostics.
Impact
An attacker can hook token storage/handling and authorization checks at runtime and observe or modify in-flight data; the backend cannot distinguish a genuine client from an instrumented or repackaged one.
Remediation
- Add anti-debug checks (Debug.isDebuggerConnected, native ptrace/TracerPid) and Frida/hooking-artefact detection.
- Integrate Play Integrity and verify the verdict server-side on sensitive operations.
- Add runtime signature/integrity self-verification to detect repackaging.
Retest
Retested build 758: FIXED. freeRASP/Talsec is integrated; libts.so performs Frida-server port scanning and detects /memfd:frida-agent, debuggable state and app-integrity/repackaging (onMalwareDetected). The app hard-blocks under Frida/instrumentation. (Client-side RASP is bypassable on a real device — inherent.)
L004 · Insufficient Anti-Reversing (Missing Code Obfuscation) — OWASP MASVS-RESILIENCE · CWE-656, CWE-540
No Code Obfuscation (Dart Symbols Recoverable)
Description
This finding reported that the Dart application code was not obfuscated (flutter build --obfuscate not used) and that a decompilation of libapp.so recovered application function bodies with original names (AuthInfo::setJwtToken, loginWithEmail, class names, source-file paths), exposing the app logic, API contracts and storage scheme.
Affected Component / Screen
lib/arm64-v8a/libapp.so — Dart AOT snapshot
Evidence
Shipped libapp.so: app class names and package:emory_mobile_app URIs = 0.
Proof of Concept
- Extract lib/arm64-v8a/libapp.so and run a Flutter reverse tool.
- strings libapp.so grep for setJwtToken/HiveAesCipher/package:emory_mobile_app returns nothing on the shipped build.
Impact
If unobfuscated, reverse engineering of API contracts, token handling and business logic is unobstructed; on the shipped artifact the app symbols are stripped.
Remediation
- Continue building release artifacts with flutter build --obfuscate --split-debug-info=<dir>; retain the mapping privately for crash de-obfuscation.
- Do not rely on obfuscation alone — pair with the runtime protections (F9–F11) and server-side authorization.
Retest
Retested / re-verified: FIXED. The shipped libapp.so is obfuscated — it contains zero package:emory_mobile_app URIs and zero app class names (HiveAesCipher, Redaction, SecureBoxKey, InspectionModel all absent); the only readable CamelCase identifiers are Flutter framework classes that --obfuscate never strips. Confirmed on both the tested Play build and 758.
L005 · 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 runtime diagnostics in an unencrypted Hive box (errorsBox) and uploads them to POST /diagnostics/errors. Stored records contain full Dart stack traces with source-file paths, the device fingerprint, build flavor, model and session identifiers in clear text. Critically, errorsBox and permissionsBox are not cleared on logout (the logout routine clears eight other boxes but omits these), so diagnostics persist across user switches on shared devices.
Affected Component / Screen
app_flutter/errorsbox.hive, permissionsbox.hive; POST /diagnostics/errors; logout logoutHelper clears 8 boxes but not errorsBox/permissionsBox
Evidence
errorsbox.hive held clear-text stack traces, source paths, device fingerprint and session IDs, and survived logout.
Proof of Concept
- Trigger any error (e.g. a failed login).
- Read errorsbox.hive with su and strings.
- Observe clear-text stack traces, source paths, device fingerprint, model and session IDs.
- Log out and re-read — the diagnostics records remain, confirming they are not cleared.
Impact
Leaks internal application architecture and source paths; persists device fingerprint and session metadata in clear text surviving logout on shared devices — a GDPR privacy concern.
Remediation
- Add errorsBox and permissionsBox to the logout/clear-data routine.
- Strip stack traces and source paths from release-build diagnostics; gate verbose journaling behind kReleaseMode==false.
- Encrypt the diagnostics box (KeyStore-backed AES) or store transiently and purge after upload.
Retest
Retested build 758: FIXED. clearInfo()/logout now also deletes the refresh token and auth model and clears errorsBox/permissionsBox (both truncated to 0 bytes on logout); no diagnostics survive logout.
L006 · Improper Platform Usage (App Link Verification Failure) — OWASP MASVS-PLATFORM · CWE-939, CWE-919
Password-Reset App Link Fails Verification and Is Deep-Link Hijackable
Description
MainActivity registers an autoVerify App Link intent-filter for https://dev-fe.emory.pro/reset-password (plus a cleartext http variant). App Links verification fails for two reasons: (1) assetlinks.json returned a 302 redirect to the www host and Android does not follow redirects during verification; (2) the build signing certificate (the Google Play App Signing key) is not the certificate listed in the published assetlinks.json. On-device verification state is 1024 (unverified). A malicious app declaring the same filter can intercept the link via the disambiguation chooser. Impact is limited today because the Flutter routing table has no /reset-password route — a forced deep link lands on /splash — so this is a latent verification/ownership gap and a poor-practice cleartext deep link to fix before any in-app reset handler is added.
Affected Component / Screen
MainActivity VIEW intent-filter for https|http://dev-fe.emory.pro/reset-password (autoVerify=true); .well-known/assetlinks.json returned 302
Evidence
assetlinks.json 302 to www (now 200); on-device link state 1024 (unverified); signing cert absent from assetlinks.
Proof of Concept
- Confirm assetlinks state: curl -sI the apex assetlinks.json (originally 302 to www; now 200).
- adb shell pm get-app-links pro.emory.inspections.dev -> dev-fe.emory.pro 1024 (unverified), signature FA:47:D0:55 absent from assetlinks.
- Launch the link via am start VIEW with a token/redirect -> app routes to /splash; no token handling occurs.
Impact
A co-resident malicious app could present a chooser and receive the /reset-password URI; today no sensitive data flows through it, but adding an in-app reset handler without fixing verification would expose the reset token. The cleartext http variant additionally allows the reset link to traverse cleartext.
Remediation
- Serve assetlinks.json directly at the apex with a 200 (no redirect) and include the exact signing-certificate SHA-256 of the distributed build (the Play App Signing key).
- Remove the http scheme from the App Link filter; keep https only.
- Re-verify with adb pm verify-app-links --re-verify.
- If/when an in-app reset handler is added, treat reset tokens as single-use, short-TTL and server-bound.
Retest
Retested build 758: PARTIALLY FIXED. The redirect leg is resolved — the www host no longer resolves and the apex now serves assetlinks.json at a direct 200 (no 302). However the certificate in assetlinks (AD:A9:9B:CB...) still does not match the delivered signing certificate (FA:47:D0:55..., the Play App Signing key), so App Links still will not verify until assetlinks lists the current signing key. No in-app reset handler was added, so live impact remains latent.
L007 · Information Exposure via Screen Capture — OWASP MASVS-PLATFORM · CWE-200
Missing Screenshot / Task-Switcher Protection (FLAG_SECURE Absent)
Description
Neither the Android shim nor the Dart layer sets WindowManager FLAG_SECURE (no screenshot-prevention plugin present). Sensitive screens — the login form, inspection data, customer details and assignee signatures — can be screenshotted, screen-recorded, and are rendered into the recent-apps thumbnail, which may be cached to disk.
Affected Component / Screen
All activities/screens (no FLAG_SECURE in MainActivity or Dart)
Evidence
Screenshots and the recents thumbnail captured sensitive screens; no FLAG_SECURE present.
Proof of Concept
- Open a sensitive screen (login or an inspection record) and take a screenshot — capture succeeds.
- Background the app and view the recents thumbnail — sensitive content is visible.
- Confirm statically: grep for FLAG_SECURE in smali returns no app match.
Impact
Sensitive content leaks via screenshots, screen recording and the recents thumbnail cache.
Remediation
- Apply FLAG_SECURE to screens rendering credentials or PII (secure_application plugin, or setFlags in MainActivity for the relevant routes).
- At minimum, mask the task-switcher preview for authenticated screens.
Retest
Retested build 758: FIXED. MainActivity.onCreate now calls getWindow().setFlags(FLAG_SECURE, FLAG_SECURE); a live screencap of the resumed app renders fully black.
L008 · Path Traversal via Filename — OWASP MASVS-PLATFORM · CWE-22
Unsanitised Provider Filename Used in Cache Path (File-Picker Path Traversal)
Description
The custom_file_picker MethodChannel handler builds a cache-file path by concatenating the content-provider-supplied display_name value without sanitisation: new File(getCacheDir(), picker + timestamp + _ + name). A content provider returning a _display_name containing ../ sequences could direct the copy outside getCacheDir() within the app sandbox. Exploitation requires the user to pick a file exposed by a malicious app content provider, so likelihood is low, but the unsanitised-filename pattern should be corrected.
Affected Component / Screen
pro.emory.inspections.MainActivity custom_file_picker channel (MainActivity.smali: const-string picker_, _display_name)
Evidence
Unsanitised _display_name concatenated into the cache path; a ../ display name could escape getCacheDir().
Proof of Concept
- Expose a content provider returning _display_name = ../databases/x with arbitrary content.
- In the app, choose that file via the custom file picker and observe whether the cache write resolves the traversal relative to getCacheDir().
Impact
Potential overwrite of files within the app own sandbox (e.g. cached databases) by an attacker-supplied file selected by the user.
Remediation
- Sanitise the display name before use — reduce to a base name and reject traversal (name = new File(name).getName(); reject / or ..).
- Generate the cache filename internally (UUID) and keep the original name only as metadata.
Retest
Retested build 758: FIXED. The handler now reduces the display name to its base name (new File(displayName).getName()) with an isBlank fallback before building the cache path; traversal sequences (../, /, ..) all resolve safely under cache/. Verified against the exact runtime logic.
L009 · Excessive Permissions (Least Privilege) — OWASP MASVS-PRIVACY · CWE-250, CWE-272
Over-Privileged READ_PHONE_STATE Permission
Description
The manifest requests READ_PHONE_STATE, a dangerous permission granting access to device identifiers and telephony state, despite the app having no telephony feature. It is contributed transitively by the permission_handler plugin phone group; no app code path requests it, so it is a declared-but-unused over-privilege that broadens the privacy footprint.
Affected Component / Screen
uses-permission android.permission.READ_PHONE_STATE (contributed by permission_handler)
Evidence
aapt dump permissions listed READ_PHONE_STATE with no telephony feature justifying it.
Proof of Concept
- Inspect the manifest: aapt dump permissions base.apk shows READ_PHONE_STATE.
- Confirm no telephony feature or Dart phone-permission request justifies it.
Impact
Unnecessary access to device identifiers; increases privacy/compliance exposure and grant-prompt fatigue.
Remediation
- Remove READ_PHONE_STATE if unused; strip the contributing dependency via uses-permission tools:node=remove.
- Audit all uses-permission declarations against actual feature use and apply least privilege.
Retest
Retested build 758: FIXED. The merged manifest no longer contains READ_PHONE_STATE (a uses-permission tools:node=remove entry strips the transitively-merged permission; confirmed absent via aapt2).
L010 · Privacy — Insufficient Control over Personal Data Collection — OWASP MASVS-PRIVACY · CWE-359
Device Fingerprint and Identity Telemetry Without Clear Consent Controls
Description
The app collects a device fingerprint and links the device to the authenticated user identity, then transmits diagnostic telemetry to the backend. A getDeviceHealthSnapshot MethodChannel gathers network status and a deviceId, batched with a session ID and uploaded to POST /diagnostics/errors and /diagnostics/. Separately, notifications_service sets the OneSignal external_container_id, binding the push identity to the in-app user. For an EU-facing product these flows require explicit consent gating and processor documentation.
Affected Component / Screen
diagnostics_upload_service.dart, device_health_snapshot.dart -> POST /diagnostics/errors, /diagnostics/; notifications_service.dart OneSignal external_container_id; OneSignal App ID in bundled .env
Evidence
Telemetry collectors (getDeviceHealthSnapshot, deviceId, external_container_id) and the /diagnostics sinks were present with no consent gate.
Proof of Concept
- Inspect the collectors: grep for getDeviceHealthSnapshot/deviceId/external_container_id in the decompiled app.
- Confirm the sinks: grep for /diagnostics returns /diagnostics/errors and /diagnostics/.
- Drive the app and observe diagnostics batches being assembled and the OneSignal identity linkage on login.
Impact
Device fingerprint, session context and a device-to-user identity linkage are collected and transmitted; without consent controls this is a GDPR concern for EU users.
Remediation
- Gate all telemetry/diagnostics upload behind explicit, revocable user consent; default to off.
- Minimise collected identifiers (avoid stable device IDs where possible) and document the OneSignal processor relationship.
- Ensure the redaction allowlist runs on every outbound diagnostics/log path before persistence and upload.
Retest
Retested build 758: FIXED. The 758 build adds a diagnostics consent dialog (diagnosticsConsentTitle/Body/Allow/Decline) and App-Tracking-Transparency handling, providing opt-in/out consent on install and in in-app settings.
I001 · Information Disclosure (Build Configuration) — OWASP MASVS-STORAGE · CWE-200
Backend Topology Disclosed in Bundled .env Asset
Description
The Flutter bundle ships its .env configuration file in clear text. It discloses the full backend topology — the active development host, the commented-out production host (prod-be.emory.pro), internal/local development IPs, and Render staging hosts — plus the OneSignal app id. It contains no private keys, but it hands an attacker a map of the environment and confirms the mobile and web clients share the same backend.
Affected Component / Screen
assets/flutter_assets/.env
Evidence
.env listed prod-be.emory.pro, internal IPs (192.168.x, 10.0.2.2, localhost), Render hosts and the OneSignal id.
Proof of Concept
- Inspect the artifact: unzip -p base.apk assets/flutter_assets/.env.
- Observe the active API_BASE_URL, commented prod-be.emory.pro, onrender.com hosts, internal IPs and ONE_SIGNAL_APP_ID.
Impact
Reveals production/dev/internal host topology, aiding targeting of other environments. Reconnaissance value only; no secret material.
Remediation
- Strip commented/unused hosts from the shipped .env; include only the single environment base URL the build targets.
- Inject configuration at build time per flavor rather than bundling a multi-environment .env.
- Treat the OneSignal app id as public but avoid co-locating it with internal host inventories.
Retest
Retested build 758: FIXED. The bundled .env is now minimised to a single active host (APP_NAME, API_BASE_URL=https://dev-be.emory.pro, BUILD_DATE, one OneSignal id); the commented production/internal/Render hosts are gone.
/ 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