Improve credential autofill matcher for mobile apps (#1477)

This commit is contained in:
Leendert de Borst
2026-01-27 16:36:47 +00:00
committed by Leendert de Borst
parent 36b3a19d13
commit 433098d89b
2 changed files with 75 additions and 1 deletions
+43 -1
View File
@@ -279,7 +279,49 @@ pub fn filter_credentials(input: CredentialMatcherInput) -> CredentialMatcherOut
}
}
// No matches found in Priority 2 or Priority 3
// ═══════════════════════════════════════════════════════════════════════════
// PRIORITY 3b: URL Word / Item Name Fallback
// No domain or page title matches found - try matching words extracted
// from the current URL against item names for credentials without URLs.
// Same anti-phishing rule: only credentials with NO URLs are eligible.
// ═══════════════════════════════════════════════════════════════════════════
let url_words = extract_words(&current_url);
if !url_words.is_empty() {
let url_word_match_ids: Vec<String> = credentials
.iter()
.filter(|cred| {
// SECURITY: Skip credentials that have URLs defined
if !cred.item_urls.is_empty()
&& cred.item_urls.iter().any(|u| !u.is_empty())
{
return false;
}
if let Some(item_name) = &cred.item_name {
let cred_name_words = extract_words(item_name);
// Match only complete words, not substrings
url_words.iter().any(|url_word| {
cred_name_words.iter().any(|cred_word| url_word == cred_word)
})
} else {
false
}
})
.map(|cred| cred.id.clone())
.take(3)
.collect();
if !url_word_match_ids.is_empty() {
return CredentialMatcherOutput {
matched_ids: url_word_match_ids,
matched_priority: 3,
};
}
}
// No matches found in Priority 2, 3, or 3b
return CredentialMatcherOutput {
matched_ids: vec![],
matched_priority: 0,
+32
View File
@@ -725,3 +725,35 @@ fn test_items_without_urls_not_matched_when_url_match_exists() {
assert_eq!(matches.len(), 1, "Should only return URL match, not title matches");
assert_eq!(matches[0].item_name.as_deref(), Some("blabla service"));
}
/// [#35] - Items WITHOUT URLs should match via URL-derived word matching on item name
#[test]
fn test_items_without_urls_matched_by_url_domain_words() {
let credentials = vec![
create_test_credential("Test Dumpert", "", "user@dumpert.nl"),
create_test_credential("Some Other Item", "", "user@other.com"),
];
// When visiting dumpert.nl with no page title, the credential named "Test Dumpert"
// should still match because "dumpert" from the URL matches "dumpert" in the item name
let matches = filter(credentials, "https://www.dumpert.nl", "");
assert_eq!(matches.len(), 1, "Should match credential by URL-derived word against item name");
assert_eq!(matches[0].item_name.as_deref(), Some("Test Dumpert"));
}
/// [#36] - URL-derived word matching should NOT match credentials that have URLs defined
#[test]
fn test_url_word_matching_skips_credentials_with_urls() {
let credentials = vec![
// This credential HAS a URL (different domain) - should NOT match via name
create_test_credential("Test Dumpert", "https://other-site.com", "user@dumpert.nl"),
// This credential has NO URL - should match via name
create_test_credential("Dumpert Account", "", "user@dumpert.nl"),
];
let matches = filter(credentials, "https://www.dumpert.nl", "");
assert_eq!(matches.len(), 1, "Should only match the credential without URLs");
assert_eq!(matches[0].item_name.as_deref(), Some("Dumpert Account"));
}