From 34e29204a3720741b4b5373cae2afbdb0e17bad2 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 09:51:18 +0100 Subject: fix: stop exporting DecryptActivity and validate decrypt paths DecryptActivity was exported without an intent filter, so any installed app could start it with arbitrary FILE_PATH/REPO_PATH extras (and crash it by omitting them). Nothing outside the app needs to launch it: shortcuts and the decrypt action go through LaunchActivity. LaunchActivity stays exported, so it now treats FILE_PATH as untrusted: the path is forwarded only when it canonically resolves to a regular file inside the password store, and REPO_PATH is always the real store directory instead of the caller-supplied value. Anything else falls back to the main screen. Co-Authored-By: Claude Opus 5.5 --- .../app/passwordstore/ui/main/LaunchActivity.kt | 25 ++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) (limited to 'app/src/main/java') diff --git a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt index 90434bb7..1a5e8a89 100644 --- a/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt @@ -11,6 +11,7 @@ import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit +import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.crypto.DecryptActivity import app.passwordstore.ui.passwords.PasswordStore @@ -18,6 +19,7 @@ import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result import app.passwordstore.util.extensions.sharedPrefs import app.passwordstore.util.settings.PreferenceKeys +import java.io.File @SuppressLint("CustomSplashScreen") class LaunchActivity : AppCompatActivity() { @@ -53,17 +55,15 @@ class LaunchActivity : AppCompatActivity() { } private fun startTargetActivity(noAuth: Boolean) { + // This activity is exported, so the file path is untrusted input: only forward it to + // DecryptActivity when it points at a file inside the password store. + val repoDir = PasswordRepository.getRepositoryDirectory() + val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH) val intentToStart = - if (intent.action == ACTION_DECRYPT_PASS) + if (intent.action == ACTION_DECRYPT_PASS && isFileInRepo(filePath, repoDir)) getDecryptIntent().apply { - putExtra( - BasePGPActivity.EXTRA_FILE_PATH, - intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH), - ) - putExtra( - BasePGPActivity.EXTRA_REPO_PATH, - intent.getStringExtra(BasePGPActivity.EXTRA_REPO_PATH), - ) + putExtra(BasePGPActivity.EXTRA_FILE_PATH, filePath) + putExtra(BasePGPActivity.EXTRA_REPO_PATH, repoDir.absolutePath) } else Intent(this, PasswordStore::class.java).setAction(Intent.ACTION_VIEW) startActivity(intentToStart) @@ -74,5 +74,12 @@ class LaunchActivity : AppCompatActivity() { companion object { const val ACTION_DECRYPT_PASS = "DECRYPT_PASS" + + /** True when [path] resolves, after following `..` and symlinks, to a file under [repoDir]. */ + fun isFileInRepo(path: String?, repoDir: File): Boolean { + if (path.isNullOrEmpty()) return false + val file = File(path).canonicalFile + return file.isFile && file.startsWith(repoDir.canonicalFile) + } } } -- cgit v1.2.3