04/26 Enhanced app and extension security
This commit is contained in:
@@ -50,21 +50,71 @@
|
||||
.filter(el => isVisible(el) && !el.disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true only if the input field carries signals suggesting it
|
||||
* collects a credential (username / email / phone) — not a generic
|
||||
* text field such as a search box, full-name field, or address field.
|
||||
*
|
||||
* Scoring precedence:
|
||||
* 1. autocomplete="username"|"email"|"tel" → definite YES
|
||||
* 2. Non-credential autocomplete value → definite NO
|
||||
* 3. name / id / placeholder / aria-label contain a credential keyword → YES
|
||||
* 4. Otherwise → NO (do not decorate)
|
||||
*/
|
||||
function _isLikelyUsernameField(el) {
|
||||
const CRED_HINTS = /user|email|mail|login|phone|tel|mobile|account/i;
|
||||
const ac = (el.getAttribute('autocomplete') || '').toLowerCase().trim();
|
||||
|
||||
// Strongest positive signal.
|
||||
if (['username', 'email', 'tel'].includes(ac)) return true;
|
||||
|
||||
// Definite negative signals (Chrome's autocomplete token set).
|
||||
const NON_CRED_AC = /^(name|given-name|family-name|additional-name|honorific-prefix|honorific-suffix|organization|street-address|address-line[123]|address-level[1234]|country|country-name|postal-code|cc-|transaction-|language|bday|sex|url|photo|search|new-password|current-password|one-time-code|off)$/i;
|
||||
if (ac && NON_CRED_AC.test(ac)) return false;
|
||||
|
||||
// Check name, id, placeholder, and aria-label for credential keywords.
|
||||
const attrs = [
|
||||
el.getAttribute('name') || '',
|
||||
el.getAttribute('id') || '',
|
||||
el.getAttribute('placeholder') || '',
|
||||
el.getAttribute('aria-label') || '',
|
||||
].join(' ');
|
||||
|
||||
return CRED_HINTS.test(attrs);
|
||||
}
|
||||
|
||||
function findUsernameField(pwField) {
|
||||
// Helper: accept email/tel inputs unconditionally; text inputs only when
|
||||
// they look like a genuine credential field.
|
||||
function isCredentialType(el) {
|
||||
if (el.type === 'email' || el.type === 'tel') return true;
|
||||
if (el.type === 'text') return _isLikelyUsernameField(el);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Walk backwards through all inputs in DOM order.
|
||||
const all = Array.from(document.querySelectorAll('input'));
|
||||
const idx = all.indexOf(pwField);
|
||||
for (let i = idx - 1; i >= 0; i--) {
|
||||
const el = all[i];
|
||||
if (!isVisible(el) || el.disabled) continue;
|
||||
if (['email', 'text', 'tel'].includes(el.type)) return el;
|
||||
if (isCredentialType(el)) return el;
|
||||
}
|
||||
// 2. Fallback: search within the same form/ancestor container.
|
||||
|
||||
// 2. Fallback: search within the same form / ancestor container.
|
||||
// Prefer email inputs first, then scored text/tel inputs.
|
||||
const container = pwField.closest('form') || pwField.closest('[role="form"]') || pwField.parentElement;
|
||||
if (container) {
|
||||
const candidate = container.querySelector('input[type="email"]:not([disabled]), input[type="text"]:not([disabled]), input[type="tel"]:not([disabled])');
|
||||
if (candidate && isVisible(candidate)) return candidate;
|
||||
const emailCandidate = container.querySelector('input[type="email"]:not([disabled])');
|
||||
if (emailCandidate && isVisible(emailCandidate)) return emailCandidate;
|
||||
|
||||
const textTelInputs = Array.from(
|
||||
container.querySelectorAll('input[type="text"]:not([disabled]), input[type="tel"]:not([disabled])')
|
||||
);
|
||||
const scored = textTelInputs.filter(el => isVisible(el) && _isLikelyUsernameField(el));
|
||||
if (scored.length) return scored[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -127,7 +177,7 @@
|
||||
var freshItems = _matchingItems;
|
||||
if (!freshItems.length) {
|
||||
try {
|
||||
var result = await chrome.storage.local.get('vault_items_cs');
|
||||
var result = await chrome.storage.session.get('vault_items_cs');
|
||||
var all = (result && result.vault_items_cs) || [];
|
||||
freshItems = _filterForHost(all);
|
||||
if (freshItems.length) _matchingItems = freshItems;
|
||||
@@ -599,11 +649,11 @@
|
||||
}
|
||||
|
||||
async function decorateFields() {
|
||||
// Read from chrome.storage.local — reliable across all Chrome versions and
|
||||
// does not depend on message delivery from the service worker.
|
||||
// Read from chrome.storage.session — memory-only, cleared on browser close.
|
||||
// Decrypted vault data must never be written to persistent (local) storage.
|
||||
var all = [];
|
||||
try {
|
||||
var result = await chrome.storage.local.get('vault_items_cs');
|
||||
var result = await chrome.storage.session.get('vault_items_cs');
|
||||
all = (result && result.vault_items_cs) || [];
|
||||
} catch (e) { }
|
||||
|
||||
@@ -621,14 +671,33 @@
|
||||
|
||||
// ── Vault item helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normalise a stored URL string so it is always parseable by `new URL()`.
|
||||
* Handles bare domains ("github.com"), protocol-relative ("//github.com"),
|
||||
* and fully-formed URLs ("https://github.com") identically.
|
||||
*/
|
||||
function _normaliseUrl(raw) {
|
||||
if (!raw) return null;
|
||||
var s = raw.trim();
|
||||
if (/^https?:\/\//i.test(s)) return s; // already has a scheme
|
||||
if (s.startsWith('//')) return 'https:' + s; // protocol-relative
|
||||
return 'https://' + s; // bare domain or path
|
||||
}
|
||||
|
||||
function _filterForHost(items) {
|
||||
var host = location.hostname.replace(/^www\./, '');
|
||||
return (items || []).filter(function (item) {
|
||||
if (item.item_type !== 'password' || !(item.plain && item.plain.url)) return false;
|
||||
try {
|
||||
var h = new URL(item.plain.url).hostname.replace(/^www\./, '');
|
||||
var normalised = _normaliseUrl(item.plain.url);
|
||||
if (!normalised) return false;
|
||||
var h = new URL(normalised).hostname.replace(/^www\./, '');
|
||||
// Match exact domain or any subdomain relationship.
|
||||
return h === host || h.endsWith('.' + host) || host.endsWith('.' + h);
|
||||
} catch (e) { return false; }
|
||||
} catch (e) {
|
||||
console.warn('[PassKeeper] _filterForHost: could not parse URL:', item.plain.url, e.message);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,7 +715,7 @@
|
||||
async function classifyCredentials(username, password) {
|
||||
var all = [];
|
||||
try {
|
||||
var result = await chrome.storage.local.get('vault_items_cs');
|
||||
var result = await chrome.storage.session.get('vault_items_cs');
|
||||
all = (result && result.vault_items_cs) || [];
|
||||
} catch (e) { return 'new'; }
|
||||
if (!all.length) return 'new';
|
||||
@@ -816,7 +885,7 @@
|
||||
// React instantly when the popup writes fresh vault data to local storage.
|
||||
// This fires in the same tick as the write — no message delivery required.
|
||||
chrome.storage.onChanged.addListener(function (changes, area) {
|
||||
if (area === 'local' && changes.vault_items_cs) {
|
||||
if (area === 'session' && changes.vault_items_cs) {
|
||||
var allItems = (changes.vault_items_cs.newValue) || [];
|
||||
_matchingItems = _filterForHost(allItems);
|
||||
console.log('[PassKeeper] storage.onChanged: matched=' + _matchingItems.length + ' of ' + allItems.length + ' items');
|
||||
|
||||
Reference in New Issue
Block a user