[MM-65720] - Add "Save as draft" button on the Share extension
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
PostButtonand 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
registerDraftUpdateObserverinsideGekidouWrapperthat observesshare.extension.draftUpdateDarwin notifications. This allows communication from theShareExtensionto the main app if it's running to send the draft data to the React Native side. - Extends iOS support for the
@mattermost/rnsharelibrary to communicate draft data from the native side to the React Native side.
Android
- Adds a new
SaveDraftButtoncomponent and introduces a new hookuseShareExtensionSubmitwhich extracts the submit logic from the PostButton from the React Native Share Extension so it can be reused in both cases. - Adds a new
AppStateHelperto track if the main app is active, since we can't rely only on the React Native context (theShareExtensionalso uses its own React Native context) - Adds
DraftDataHelperandDraftdatabase extension to handle insert/update drafts in the database from the native side. - Adds
DraftWorkerwhich is enqueued from theShareWorkerto determine whether the draft should be stored from the native side or the React Native side.
React Native
- Adds a new
DraftsUpdatesingleton that listens for nativeonDraftUpdatedevents from@mattermost/rnshareand 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 |
|---|---|
| iPad |
|---|
Release Note
* Added a new "Save as draft" functionality when sharing media through Mattermost
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.
Hey @enahum 👋 Thank you so much for the feedback! I'll take a look at your suggestions and address them.
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!
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!
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.
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!