me.bluemail.mail

0
0
0
public

BlueMail - Arbitrary file write via unsanitized share-intent _display_name (CWE-22)

Unprivileged arbitrary file write into BlueMail’s private storage from any co-located app,
via path traversal in the share-intent file copy. No permissions, attacker-app-initiated.

Target BlueMail for Android - me.bluemail.mail
Version 2.2.305 (versionCode 43203), minSdk 24, targetSdk 36
Class CWE-22 Path Traversal (“Dirty Stream”)
Vulnerable component bundled react-native-receive-sharing-intent - FileDirectory.getDataColumn (also getFileFromUri)
Entry point exported com.trtf.blue.MainActivity, ACTION_SEND / ACTION_SEND_MULTIPLE, <data android:mimeType="*/*">
Impact arbitrary write to app-private storage; config / state / DB tampering
Severity CVSS:3.1 AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L ~6.1 (Medium).

Summary

com.trtf.blue.MainActivity is exported with a */* ACTION_SEND filter. When it receives a SEND
with EXTRA_STREAM, it defers to the bundled react-native-receive-sharing-intent module, which
copies the shared content into BlueMail’s storage. The destination filename is taken verbatim from
the attacker-controlled _display_name column and concatenated onto getCacheDir() with no
sanitization. A leading ../ escapes the cache directory and writes anywhere under the app’s
private data dir. A malicious app with zero permissions can drop arbitrary bytes into
/data/user/0/me.bluemail.mail/.

Root cause

Decompiled / reconstructed sink (react-native-receive-sharing-intent FileDirectory.getDataColumn):

Cursor c = ctx.getContentResolver()
              .query(uri, new String[]{ "_display_name" }, selection, selectionArgs, null);
if (c != null && c.moveToFirst()) {
    String name = c.getString(c.getColumnIndexOrThrow("_display_name")); // attacker controlled
    Log.i("FileDirectory", "File name: " + name);
    file = new File(ctx.getCacheDir(), name);                            // <-- no sanitization
}
// ...
InputStream in   = ctx.getContentResolver().openInputStream(uri);
FileOutputStream out = new FileOutputStream(file);                       // opens traversed path
ByteStreamsKt.copyTo(in, out, 8192);                                     // attacker bytes written
return file.getPath();

Between the cursor read and new File(...) there is only a Log.i call. No getName(), no
getCanonicalPath() containment check, no .. / separator stripping (confirmed at bytecode level).
getCacheDir() is /data/user/0/me.bluemail.mail/cache, so name = "../<x>" resolves into the
app data root.

A second traversable path exists in the same class - getFileFromUri names the copy from
Uri.getLastPathSegment() (decoded), reachable via an encoded %2e%2e%2f last segment. It is the
fallback route; the _display_name route is cleaner.

Reachability

attacker app  --ACTION_SEND (explicit component, no chooser)-->
  com.trtf.blue.MainActivity            [exported=true, <data mimeType="*/*">]
    handleMailtoIntent: "SEND intent has EXTRA_STREAM, deferring to share intent handler"
  -> ReceiveSharingIntentModule.getFileNames(Promise)         [@ReactMethod, BlueMail JS pulls the item]
  -> ReceiveSharingIntentHelper.sendFileNames -> getMediaUris
       uri = intent.getParcelableExtra("android.intent.extra.STREAM")    // attacker controlled
  -> FileDirectory.getAbsolutePath(ctx, uri)
       !DocumentsContract.isDocumentUri(...) && scheme == "content"
         -> getDataColumn(ctx, uri, null, null)
  -> FileDirectory.getDataColumn(...)                          // SINK (above)

Call chain is xref-confirmed live. The write executes when BlueMail’s compose flow requests the
shared item (getReceivedFiles), before any send and before any draft save.

PoC

image

Marker bytes land in the data root, one level above cache/. Control case: with TRAVERSAL_NAME
set to a plain filename (no ../), the file lands in cache/, proving the ../ is what moves it.

Impact

Arbitrary write into BlueMail’s private storage by any installed app, no permissions, no user
action beyond the share being handled. The ../ converts the library’s intended ephemeral cache/
write into a durable write to the data root - it survives cache eviction, persisting until
app-data clear or uninstall. Reachable targets include:

  • shared_prefs/*.xml and the bundled MMKV stores - flip flags / tamper settings.
  • databases/* - the app_addresses table (addresses, display_name, is_display_name_override,
    notification / verify state) is account/identity state; corrupting it is a concrete integrity demo.
  • any app-private file - corruption / DoS.

Remediation

Sanitize the resolved name before building the destination in both getDataColumn and
getFileFromUri:

  • reduce to basename (new File(name).getName()), or
  • reject names containing .. or path separators, and
  • enforce canonical-path containment: dest.getCanonicalPath().startsWith(cacheDir.getCanonicalPath() + File.separator).

This is a defect in react-native-receive-sharing-intent. Integrators should pin a fixed version
or pre-sanitize the share payload. Independently, narrowing the exported MainActivity SEND filter
reduces exposure.

v0.3.3[beta]