Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class AutofillSaveActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val repo = PasswordRepository.getRepositoryDirectory()
val saveRoot = AutofillPreferences.saveDirectory(this)?.let(repo::resolve) ?: repo
val saveIntent =
Intent(this, PasswordCreationActivity::class.java).apply {
putExtras(
Expand All @@ -136,7 +137,7 @@ class AutofillSaveActivity : AppCompatActivity() {
putString(BasePGPActivity.EXTRA_REPO_PATH, repo.absolutePath)
putString(
BasePGPActivity.EXTRA_FILE_PATH,
repo
saveRoot
.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME) ?: throw NullPointerException())
.absolutePath,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
summaryProvider = { activity.getString(R.string.preference_custom_public_suffixes_summary) }
textInputHintRes = R.string.preference_custom_public_suffixes_hint
}
editText(PreferenceKeys.AUTOFILL_SAVE_DIRECTORY) {
dependency = PreferenceKeys.AUTOFILL_ENABLE
titleRes = R.string.preference_autofill_save_directory_title
summaryProvider = { value ->
activity.getString(
R.string.preference_autofill_save_directory_summary,
value?.takeUnless { it.isBlank() }
?: activity.getString(R.string.preference_autofill_save_directory_root_placeholder),
)
}
textInputHintRes = R.string.preference_autofill_save_directory_hint
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,34 @@ object AutofillPreferences {
return DirectoryStructure.fromValue(value)
}

/**
* The configured relative directory Autofill-saved credentials should be placed under. Backed by
* [PreferenceKeys.AUTOFILL_SAVE_DIRECTORY]. Returns null when unset, or when the configured value
* is not a valid relative path (absolute, or containing "." or ".." components) — callers should
* treat a null result as the repository root itself and resolve a non-null result against it.
* This only affects saves made through the Autofill framework (the system "Save to Password
* Store?" prompt) — it has no effect on entries created from within the app.
*/
fun saveDirectory(context: Context): String? {
return sanitizeSaveDirectory(
context.sharedPrefs.getString(PreferenceKeys.AUTOFILL_SAVE_DIRECTORY)
)
}

/**
* Validates [value] as a relative path with no "." or ".." components, returning it unchanged if
* valid. Invalid or blank input is rejected outright (returns null) rather than being mutated
* into some other path, so a rejected value always falls back to the repository root instead of
* silently landing somewhere the user didn't ask for.
*/
internal fun sanitizeSaveDirectory(value: String?): String? {
if (value.isNullOrBlank()) return null
if (value.startsWith('/')) return null
val segments = value.split('/')
if (segments.any { it.isEmpty() || it == "." || it == ".." }) return null
return segments.joinToString("/")
}

fun strictDomainSearch(context: Context): Boolean {
return context.sharedPrefs.getBoolean(PreferenceKeys.STRICT_DOMAIN_SEARCH, true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ object PreferenceKeys {
const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes"
const val OREO_AUTOFILL_DEFAULT_USERNAME = "oreo_autofill_default_username"
const val DIRECTORY_STRUCTURE = "oreo_autofill_directory_structure"
const val AUTOFILL_SAVE_DIRECTORY = "oreo_autofill_save_directory"
const val STRICT_DOMAIN_SEARCH = "oreo_autofill_strict_domain_search"
const val PREF_KEY_PWGEN_TYPE = "pref_key_pwgen_type"
const val REPOSITORY_INITIALIZED = "repository_initialized"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@
<string name="preference_custom_public_suffixes_title">Custom domains</string>
<string name="preference_custom_public_suffixes_summary">Autofill will distinguish subdomains of these domains.</string>
<string name="preference_custom_public_suffixes_hint">company.com\npersonal.com</string>
<string name="preference_autofill_save_directory_title">Save directory</string>
<string name="preference_autofill_save_directory_summary">Credentials saved via Autofill are placed under this folder instead of the store root. Currently: %1$s</string>
<string name="preference_autofill_save_directory_hint">e.g. www</string>
<string name="preference_autofill_save_directory_root_placeholder">store root</string>

<!-- Password creation/edit -->
<string name="password_creation_edit_file_encryption_success_title">Password item edited</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright © 2014-2026 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.util.autofill

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

class AutofillPreferencesTest {

@Test
fun nullOrEmptyFallsBackToRoot() {
assertNull(AutofillPreferences.sanitizeSaveDirectory(null))
assertNull(AutofillPreferences.sanitizeSaveDirectory(""))
assertNull(AutofillPreferences.sanitizeSaveDirectory(" "))
}

@Test
fun acceptsSingleSegment() {
assertEquals("www", AutofillPreferences.sanitizeSaveDirectory("www"))
}

@Test
fun acceptsNestedSegments() {
assertEquals("www/personal", AutofillPreferences.sanitizeSaveDirectory("www/personal"))
}

@Test
fun rejectsAbsolutePath() {
assertNull(AutofillPreferences.sanitizeSaveDirectory("/www"))
}

@Test
fun rejectsLeadingTraversal() {
assertNull(AutofillPreferences.sanitizeSaveDirectory("../www"))
}

@Test
fun rejectsEmbeddedTraversal() {
assertNull(AutofillPreferences.sanitizeSaveDirectory("www/../personal"))
}

@Test
fun rejectsCurrentDirectoryComponent() {
assertNull(AutofillPreferences.sanitizeSaveDirectory("."))
assertNull(AutofillPreferences.sanitizeSaveDirectory("www/./personal"))
}

@Test
fun rejectsEmptyPathComponents() {
assertNull(AutofillPreferences.sanitizeSaveDirectory("www//personal"))
assertNull(AutofillPreferences.sanitizeSaveDirectory("www/"))
}
}