Aug 26 - Update password detect against off field
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
This commit is contained in:
+132
-38
@@ -99,8 +99,21 @@
|
||||
if (["username", "email", "tel"].includes(ac)) return true;
|
||||
|
||||
// Definite negative signals (Chrome's autocomplete token set).
|
||||
//
|
||||
// "off" is deliberately NOT in this list. It says nothing about whether a
|
||||
// field holds a credential — routers, banks and admin panels set it on
|
||||
// login inputs precisely to discourage password managers, and every major
|
||||
// password manager (and Chrome itself, for password fields) ignores it.
|
||||
// Treating it as a negative signal meant a field as obvious as
|
||||
// <input type="text" id="login_username" placeholder="Username"
|
||||
// autocomplete="off">
|
||||
// was rejected before the keyword check below ever ran.
|
||||
//
|
||||
// Letting "off" fall through is safe: the field still has to carry a
|
||||
// credential keyword AND sit near a password input (_hasPasswordSibling)
|
||||
// before it is decorated.
|
||||
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;
|
||||
/^(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)$/i;
|
||||
if (ac && NON_CRED_AC.test(ac)) return false;
|
||||
|
||||
// Check name, id, placeholder, and aria-label for credential keywords.
|
||||
@@ -1134,51 +1147,132 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Form submission watch ─────────────────────────────────────────────────────
|
||||
// ── Credential capture / save prompt ─────────────────────────────
|
||||
|
||||
// Guards against two triggers firing for the same login (e.g. a click handler
|
||||
// that also submits a form). Cleared after a short window.
|
||||
var _captureCooldown = false;
|
||||
|
||||
/**
|
||||
* Collect the credentials currently entered within `scope` and offer to save
|
||||
* them. `scope` is the <form> for a real submit, or the document for the
|
||||
* fallback triggers below.
|
||||
*/
|
||||
async function maybeCaptureCredentials(scope) {
|
||||
if (_captureCooldown) return;
|
||||
|
||||
var root = scope || document;
|
||||
var pwField = Array.prototype.find.call(
|
||||
root.querySelectorAll('input[type="password"]:not([disabled])'),
|
||||
function (el) {
|
||||
return isVisible(el) && el.value;
|
||||
},
|
||||
);
|
||||
if (!pwField) return;
|
||||
|
||||
var userField =
|
||||
findUsernameField(pwField) ||
|
||||
root.querySelector('input[type="email"]:not([disabled])') ||
|
||||
root.querySelector('input[type="text"]:not([disabled])');
|
||||
|
||||
var username =
|
||||
(userField && userField.value && userField.value.trim()) || "";
|
||||
var password = pwField.value;
|
||||
if (!username || !password) return;
|
||||
|
||||
_captureCooldown = true;
|
||||
setTimeout(function () {
|
||||
_captureCooldown = false;
|
||||
}, 2000);
|
||||
|
||||
removeDropdown();
|
||||
|
||||
// Check blocklist before doing anything else.
|
||||
if (await isBlocked(location.hostname)) {
|
||||
console.log(
|
||||
"[PassKeeper] Site is blocklisted, skipping save banner:",
|
||||
location.hostname,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialState = await classifyCredentials(username, password);
|
||||
console.log(
|
||||
"[PassKeeper] Credential state for",
|
||||
location.hostname,
|
||||
"→",
|
||||
credentialState,
|
||||
);
|
||||
if (credentialState === "same") return;
|
||||
|
||||
setTimeout(function () {
|
||||
showSaveBanner(username, password, credentialState);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this element look like the control that submits a login?
|
||||
*
|
||||
* Needed because many login UIs never use a <form> at all — the ASUS router
|
||||
* admin page, for example, submits with
|
||||
* <div class="button" onclick="preLogin();">Sign In</div>
|
||||
* so no "submit" event is ever dispatched and the save prompt never appeared.
|
||||
*/
|
||||
function _looksLikeSubmitControl(el) {
|
||||
if (!el || !el.tagName) return false;
|
||||
var tag = el.tagName.toUpperCase();
|
||||
if (tag === "BUTTON") return true;
|
||||
if (tag === "INPUT" && /^(submit|button|image)$/i.test(el.type)) return true;
|
||||
if (el.getAttribute("role") === "button") return true;
|
||||
// Non-semantic controls: an inline click handler, or a button-ish class.
|
||||
if (el.hasAttribute("onclick")) return true;
|
||||
var cls = (el.getAttribute("class") || "").toLowerCase();
|
||||
return /(^|[\s_-])(btn|button|submit|login|signin|sign-in)([\s_-]|$)/.test(cls);
|
||||
}
|
||||
|
||||
function watchSubmissions() {
|
||||
// 1. Real form submits.
|
||||
document.addEventListener(
|
||||
"submit",
|
||||
async function (e) {
|
||||
var form = e.target;
|
||||
var pwField = form.querySelector(
|
||||
'input[type="password"]:not([disabled])',
|
||||
);
|
||||
if (!pwField || !pwField.value) return;
|
||||
function (e) {
|
||||
maybeCaptureCredentials(e.target);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
var userField =
|
||||
findUsernameField(pwField) ||
|
||||
form.querySelector('input[type="email"]:not([disabled])') ||
|
||||
form.querySelector('input[type="text"]:not([disabled])');
|
||||
|
||||
var username =
|
||||
(userField && userField.value && userField.value.trim()) || "";
|
||||
var password = pwField.value;
|
||||
if (!username || !password) return;
|
||||
|
||||
removeDropdown();
|
||||
|
||||
// Check blocklist before doing anything else.
|
||||
if (await isBlocked(location.hostname)) {
|
||||
console.log(
|
||||
"[PassKeeper] Site is blocklisted, skipping save banner:",
|
||||
location.hostname,
|
||||
);
|
||||
return;
|
||||
// 2. Clicks on anything that looks like a submit control. Required for the
|
||||
// common case of a login UI built without a <form>, where no submit
|
||||
// event fires and the save prompt would otherwise never appear.
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
var node = e.target;
|
||||
for (var i = 0; i < 5 && node && node !== document; i++) {
|
||||
if (_looksLikeSubmitControl(node)) {
|
||||
// Let the page's own handler run first.
|
||||
setTimeout(function () {
|
||||
maybeCaptureCredentials(document);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
var credentialState = await classifyCredentials(username, password);
|
||||
console.log(
|
||||
"[PassKeeper] Credential state for",
|
||||
location.hostname,
|
||||
"\u2192",
|
||||
credentialState,
|
||||
);
|
||||
if (credentialState === "same") return;
|
||||
|
||||
// 3. Enter pressed inside a credential field — the other way such forms
|
||||
// get submitted without a <form>.
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
function (e) {
|
||||
if (e.key !== "Enter") return;
|
||||
var el = e.target;
|
||||
if (!el || el.tagName !== "INPUT") return;
|
||||
if (el.type !== "password" && !_isLikelyUsernameField(el)) return;
|
||||
setTimeout(function () {
|
||||
showSaveBanner(username, password, credentialState);
|
||||
}, 500);
|
||||
maybeCaptureCredentials(document);
|
||||
}, 0);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user