aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/src/main/AndroidManifest.xml2
-rw-r--r--app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt25
-rw-r--r--app/src/test/java/app/passwordstore/ui/main/LaunchActivityTest.kt45
3 files changed, 62 insertions, 10 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d575c7f8..407697cf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -51,7 +51,7 @@
<activity
android:name=".ui.crypto.DecryptActivity"
- android:exported="true" />
+ android:exported="false" />
<activity
android:name=".ui.main.LaunchActivity"
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)
+ }
}
}
diff --git a/app/src/test/java/app/passwordstore/ui/main/LaunchActivityTest.kt b/app/src/test/java/app/passwordstore/ui/main/LaunchActivityTest.kt
new file mode 100644
index 00000000..3c30071a
--- /dev/null
+++ b/app/src/test/java/app/passwordstore/ui/main/LaunchActivityTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package app.passwordstore.ui.main
+
+import app.passwordstore.ui.main.LaunchActivity.Companion.isFileInRepo
+import java.io.File
+import java.nio.file.Files
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class LaunchActivityTest {
+
+ @Test
+ fun onlyAcceptsFilesInsideTheRepository() {
+ val root = Files.createTempDirectory("aps").toFile()
+ val repo = File(root, "store").apply { mkdirs() }
+ val inside =
+ File(repo, "site/login.gpg").apply {
+ parentFile!!.mkdirs()
+ writeText("x")
+ }
+ val outside = File(root, "secret.gpg").apply { writeText("x") }
+ val sibling =
+ File(root, "store-evil/login.gpg").apply {
+ parentFile!!.mkdirs()
+ writeText("x")
+ }
+ val link = File(repo, "link.gpg").toPath()
+ Files.createSymbolicLink(link, outside.toPath())
+
+ assertTrue(isFileInRepo(inside.absolutePath, repo))
+ assertFalse(isFileInRepo(outside.absolutePath, repo))
+ assertFalse(isFileInRepo("${repo.absolutePath}/../secret.gpg", repo))
+ assertFalse(isFileInRepo(sibling.absolutePath, repo))
+ assertFalse(isFileInRepo(link.toString(), repo))
+ assertFalse(isFileInRepo(repo.absolutePath, repo))
+ assertFalse(isFileInRepo(File(repo, "missing.gpg").absolutePath, repo))
+ assertFalse(isFileInRepo(null, repo))
+ assertFalse(isFileInRepo("", repo))
+ root.deleteRecursively()
+ }
+}