diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-02 09:51:18 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-02 09:51:18 +0100 |
| commit | 34e29204a3720741b4b5373cae2afbdb0e17bad2 (patch) | |
| tree | 56d5107c713035674fbdf0ad8f033f384d8d117d /app/src/main | |
| parent | fda98e40f68539e5e50d29158bb5b446e6c41d5d (diff) | |
| download | HardenedPass-34e29204a3720741b4b5373cae2afbdb0e17bad2.tar.gz HardenedPass-34e29204a3720741b4b5373cae2afbdb0e17bad2.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main')
| -rw-r--r-- | app/src/main/AndroidManifest.xml | 2 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/main/LaunchActivity.kt | 25 |
2 files changed, 17 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) + } } } |
