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/src/main/AndroidManifest.xml | 2 +- .../app/passwordstore/ui/main/LaunchActivity.kt | 25 +++++++----- .../passwordstore/ui/main/LaunchActivityTest.kt | 45 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 app/src/test/java/app/passwordstore/ui/main/LaunchActivityTest.kt (limited to 'app/src') 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 @@ + android:exported="false" /> + * 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() + } +} -- cgit v1.2.3