mattermost-mobile icon indicating copy to clipboard operation
mattermost-mobile copied to clipboard

[MM-65720] - Add "Save as draft" button on the Share extension

Open geriux opened this issue 4 months ago • 6 comments

Summary

Adds a new Save as a draft button when sharing media through Mattermost, this functionality has two different implementations, for iOS it has a native Share Extension and for Android it uses React Native. It works whether the app is closed, or running in the background.

iOS

  • Updates the current PostButton and adds a new Save as draft variant to reuse the current logic.
  • Updates Gekidou to insert/update drafts in the database from the native side when the app is closed.
  • Adds registerDraftUpdateObserver inside GekidouWrapper that observes share.extension.draftUpdate Darwin notifications. This allows communication from the ShareExtension to the main app if it's running to send the draft data to the React Native side.
  • Extends iOS support for the @mattermost/rnshare library to communicate draft data from the native side to the React Native side.

Android

  • Adds a new SaveDraftButton component and introduces a new hook useShareExtensionSubmit which extracts the submit logic from the PostButton from the React Native Share Extension so it can be reused in both cases.
  • Adds a new AppStateHelper to track if the main app is active, since we can't rely only on the React Native context (the ShareExtension also uses its own React Native context)
  • Adds DraftDataHelper and Draft database extension to handle insert/update drafts in the database from the native side.
  • Adds DraftWorker which is enqueued from the ShareWorker to determine whether the draft should be stored from the native side or the React Native side.

React Native

  • Adds a new DraftsUpdate singleton that listens for native onDraftUpdated events from @mattermost/rnshare and updates the local database by inserting/updating draft messages and attaching uploaded files when the app is running.

Ticket Link

https://mattermost.atlassian.net/browse/MM-65720

Checklist

  • [ ] Added or updated unit tests (required for all new features)
  • [x] Has UI changes
  • [x] Includes text changes and localization file updates
  • [ ] Have tested against the 5 core themes to ensure consistency between them.
  • [ ] Have run E2E tests by adding label E2E iOS tests for PR.

Testing

iOS
  • Open the photos app
  • Select some images
  • Tap on the share button
  • Tap on the Mattermost app
  • Expect to see the images added
  • Type a message and select a channel
  • Tap on Save as draft
  • Expect the Share extension to close
  • Open the app
  • Go to the channel you selected for the draft
  • The draft message and media should be visible
Android
  • Open the web broswer
  • Go to any site
  • Share the site into the Mattermost app
  • Expect the Share modal to show up
  • Type a message and select a channel
  • Tap on Save as draft
  • Expect the Share modal to close
  • Open the app
  • Expect to see the new draft in the sidebar and under the Drafts list

Device Information

This PR was tested on:

  • iPhone 16 Pro Simulator, 18.4
  • iPad Simulator, 18.4
  • Pixel 8 Pro Emulator, 16.0

Screenshots

iOS Android
iOS Android
iOS Android
iPad
iPad

Release Note

* Added a new "Save as draft" functionality when sharing media through Mattermost

geriux avatar Oct 02 '25 14:10 geriux

Hello @geriux,

Thanks for your pull request! A Core Committer will review your pull request soon. For code contributions, you can learn more about the review process here.

mattermost-build avatar Oct 02 '25 14:10 mattermost-build

Hey @enahum 👋 Thank you so much for the feedback! I'll take a look at your suggestions and address them.

geriux avatar Oct 07 '25 09:10 geriux

Hey @enahum !

After reviewing your feedback I understand your concerns and it makes sense. For iOS I'm planning to adjust the implementation so that the behavior depends on whether the main app is running.

Thank you for pointing me to the push notifications functionality, I'll be using Gekidou's ApplicationIsRunning preference to decide how drafts should be handled.

If the app is not running, the share extension will update the draft directly in the database as before. First it creates the draft and if there are any files to be uploaded they will be updated into the draft after they're done uploading.

If the app is running, the database will not be updated from the extension. Instead I'm going to extend the current @mattermost/rnshare library to also support iOS. So the new flow will be: the extension sends a Darwin notification when a draft needs to be added or updated -> the main app listens for it and triggers an event through rnshare -> the React Native side receives an event with the draft data and updates the database safely.

With this scenario as you mentioned, it solves the note I added regarding iOS not refreshing the data with the app opened.

I'll follow up with an Android solution after this.

Thanks again!

geriux avatar Oct 07 '25 22:10 geriux

Hey @enahum 👋

I've addressed all of the concerns from your initial review and also updated the PR description with a summary of the latest changes.

The app now handles storing drafts whether it's closed or running. The initial draft message is stored right away and if there are any uploads, the draft gets updated with the files data once those complete.

This feature now works on both iOS and Android. Let me know what you think about this approach, any additional feedback is appreciated. Thanks again!

geriux avatar Oct 09 '25 18:10 geriux

DryRun Security

This pull request introduces multiple IPC and input-validation issues across iOS and Android: iOS Darwin notifications and shared UserDefaults allow other apps to read or inject draft data (leading to info disclosure, crashes, or DoS), unsafe forced unwrapping can crash on malformed shared data, and Android code exposes risks from unsanitized serverUrl path traversal, race conditions around app state, unbounded JSON processing, and verbose stack-trace logging that may leak internal details.

IPC Vulnerability via Darwin Notifications in ios/GekidouWrapper.swift
Vulnerability IPC Vulnerability via Darwin Notifications
Description The application uses Darwin Notifications as an Inter-Process Communication (IPC) mechanism to trigger draft updates. The notification handler reads a payload from Preferences.default (assumed to be shared UserDefaults via an App Group, making it accessible to other apps). This payload is then passed through the native iOS layer to the React Native layer without sufficient validation of its contents. An attacker's app, by sending the specific Darwin Notification and writing a crafted dictionary to the shared preferences key, can inject malicious data, leading to application crashes, data corruption, or denial of service.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/ios/GekidouWrapper.swift#L58-L61

Denial of Service via Forced Unwrapping in ios/Gekidou/Sources/Gekidou/Networking/ShareExtension.swift
Vulnerability Denial of Service via Forced Unwrapping
Description The fromDictionary method in UploadSessionData uses forced unwrapping (as!) for several properties when parsing data from a dictionary. This dictionary is loaded from UserDefaults, a persistent storage. If an attacker can manipulate the data in UserDefaults (e.g., on a compromised device), they can introduce malformed or missing values for these keys. When the application attempts to read this corrupted data, the forced unwrapping will cause a runtime crash, leading to a persistent denial of service.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/ios/Gekidou/Sources/Gekidou/Networking/ShareExtension.swift#L38-L41

Sensitive Information Disclosure via IPC in ios/Gekidou/Sources/Gekidou/Networking/ShareExtension+Post.swift
Vulnerability Sensitive Information Disclosure via IPC
Description The handleDraftWhileAppRunning function in ShareExtension+Post.swift writes draft data, including channelId, message (user-entered text), and files (file metadata like names), to Preferences.default. This Preferences.default is assumed to be a shared UserDefaults container accessible via an App Group, as implied by the vulnerability description and common iOS share extension patterns. Subsequently, a Darwin notification named share.extension.draftUpdate is posted. Any other application on the device can listen for this system-wide notification and, upon receiving it, read the sensitive draft content from the shared UserDefaults container. This allows unauthorized access to user messages and file details.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/ios/Gekidou/Sources/Gekidou/Networking/ShareExtension+Post.swift#L190-L194

Race Condition in Global State in android/app/src/main/java/com/mattermost/helpers/AppStateHelper.kt
Vulnerability Race Condition in Global State
Description The isMainAppActive variable, while marked @Volatile for visibility, is used in a 'check-then-act' pattern across different threads without proper synchronization. The DraftWorker checks isMainAppActive to decide whether to handle a draft in the foreground (via React Native bridge) or background (via database). If the main app's activity state changes (e.g., onDestroy is called) between the DraftWorker's check and its subsequent action, the worker might attempt to interact with an invalid or non-existent React Native bridge, leading to crashes or data loss/inconsistency.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/android/app/src/main/java/com/mattermost/helpers/AppStateHelper.kt#L4-L5

Arbitrary Database Access via `serverUrl` in android/app/src/main/java/com/mattermost/helpers/DraftDataHelper.kt
Vulnerability Arbitrary Database Access via serverUrl
Description The serverUrl parameter, which is used to determine the database file path, is received from the React Native frontend. While the getDatabaseForServer function uses a fixed directory structure and appends the serverUrl as a filename, it does not sanitize the serverUrl for path traversal characters (e.g., ../). An attacker could potentially craft a malicious serverUrl containing path traversal sequences to access or create database files outside the intended application data directory, leading to data leakage or corruption.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/android/app/src/main/java/com/mattermost/helpers/DraftDataHelper.kt#L20-L23

Information Disclosure via Stack Trace in android/app/src/main/java/com/mattermost/helpers/DraftDataHelper.kt
Vulnerability Information Disclosure via Stack Trace
Description The saveDraft function in DraftDataHelper.kt uses e.printStackTrace() to log exceptions. This method prints the full stack trace to standard error, which can expose sensitive internal details of the application, including file paths, class names, and potentially configuration details, if an unexpected error occurs during draft saving. This information could be valuable to an attacker for reconnaissance.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/android/app/src/main/java/com/mattermost/helpers/DraftDataHelper.kt#L27-L30

Information Disclosure via Stack Traces in android/app/src/main/java/com/mattermost/helpers/database_extension/Draft.kt
Vulnerability Information Disclosure via Stack Traces
Description The Draft.kt file contains multiple functions (insertDraft, getDraft, insertOrUpdateDraft) that catch generic exceptions and call e.printStackTrace(). This method prints the full stack trace to the standard error stream, which can expose internal application details (class names, method names, file paths, line numbers) in a production environment. This information could aid an attacker in understanding the application's structure and identifying potential attack vectors.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/android/app/src/main/java/com/mattermost/helpers/database_extension/Draft.kt#L40-L43

Unvalidated Input Processing in Worker in android/app/src/main/java/com/mattermost/rnbeta/DraftWorker.kt
Vulnerability Unvalidated Input Processing in Worker
Description The DraftWorker processes draft_data which originates from user-controlled input in the share extension. While the JSONObject(draftJson) parsing is wrapped in a try-catch block, a very large or deeply nested JSON payload could still lead to resource exhaustion (e.g., out-of-memory errors) before or during parsing, potentially causing a denial of service for the background task. The current error handling logs the exception but does not prevent the resource exhaustion itself.

https://github.com/mattermost/mattermost-mobile/blob/38fe11dfc888616aafe23cc5227fe09205dddc45/android/app/src/main/java/com/mattermost/rnbeta/DraftWorker.kt#L24-L27


All finding details can be found in the DryRun Security Dashboard.

dryrunsecurity[bot] avatar Oct 09 '25 18:10 dryrunsecurity[bot]

This PR has been automatically labelled "stale" because it hasn't had recent activity. A core team member will check in on the status of the PR to help with questions. Thank you for your contribution!

mattermost-build avatar Oct 25 '25 01:10 mattermost-build