_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). |
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/.
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.
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.
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.
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,Sanitize the resolved name before building the destination in both getDataColumn and
getFileFromUri:
new File(name).getName()), or.. or path separators, anddest.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.