From 89d88621f2b60af77307d04bc4170979d31cba23 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:24:28 +0400 Subject: [PATCH] android: prevent imported archive from being written outside the export folder --- .../chat/simplex/common/views/helpers/Utils.android.kt | 3 ++- .../chat/simplex/common/views/database/DatabaseView.kt | 5 +++++ apps/multiplatform/spec/database.md | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 141d2d2665..de6834ba41 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -233,7 +233,8 @@ actual fun getFileName(uri: URI): String? { val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) cursor.moveToFirst() // Can make an exception - cursor.getString(nameIndex) + // the provider controls this value, and callers use it as a bare file name + cursor.getString(nameIndex)?.let { File(it).name } } } catch (e: Exception) { null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 80f97d1caf..241826ac41 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -753,6 +753,11 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? { if (inputStream != null && archiveName != null) { val archivePath = "$databaseExportDir${File.separator}$archiveName" val destFile = File(archivePath) + // resolves symlinks, so it also catches a final component linking outside the folder + if (destFile.canonicalFile.parentFile != databaseExportDir.canonicalFile) { + Log.e(TAG, "saveArchiveFromURI path outside of export folder") + return null + } Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) archivePath } else { diff --git a/apps/multiplatform/spec/database.md b/apps/multiplatform/spec/database.md index f6ecedb721..8981fe7055 100644 --- a/apps/multiplatform/spec/database.md +++ b/apps/multiplatform/spec/database.md @@ -345,7 +345,7 @@ class ArchiveConfig( ### Import Flow 1. User selects an archive file. -2. UI copies it to a temp location and constructs an `ArchiveConfig`. +2. UI copies it into `databaseExportDir` and constructs an `ArchiveConfig`. The destination is confined to that folder: `getFileName` returns a bare file name on every platform, and `saveArchiveFromURI` checks the canonical destination before copying. 3. Calls `apiImportArchive(config)` which sends `CC.ApiImportArchive` to the Haskell core. 4. The core extracts and replaces both databases. 5. Returns `CR.ArchiveImported` with a list of `ArchiveError` (non-fatal issues during import).