Aug 26 - Update password detect against off field 2
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:
+130
-16
@@ -32,10 +32,17 @@
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function escHtml(str) {
|
||||
// " and ' are both required: templates in this file use a mix
|
||||
// of double- and single-quoted attributes, and an unescaped quote of
|
||||
// either kind lets injected text break out of an attribute.
|
||||
// This copy previously escaped neither, while injecting attacker-influenced
|
||||
// values (site hostname, stored item names) into attributes.
|
||||
return String(str ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +81,69 @@
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* True when this page is safe enough to put a stored credential into.
|
||||
*
|
||||
* The extension holds http://*\/* permission on purpose: a great many devices
|
||||
* that genuinely need a password manager — routers, NAS boxes, printers,
|
||||
* self-hosted admin panels — are only reachable over plain HTTP on the local
|
||||
* network, and dropping the permission would make PassKeeper useless exactly
|
||||
* where people reuse weak passwords most.
|
||||
*
|
||||
* What is NOT acceptable is filling a credential into a plaintext page on the
|
||||
* public internet, where anyone on the path can read it. Loopback and RFC1918
|
||||
* / RFC4193 / link-local addresses and .local names are treated as acceptable;
|
||||
* every other http:// origin gets a warning in the dropdown before the user
|
||||
* chooses an item.
|
||||
*/
|
||||
function _isTrustworthyOrigin() {
|
||||
if (location.protocol === "https:" || location.protocol === "file:") return true;
|
||||
var h = (location.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
|
||||
|
||||
if (h === "localhost" || h.endsWith(".localhost")) return true;
|
||||
// Reserved TLDs that cannot be registered publicly.
|
||||
if (/\.(local|lan|home|internal)$/.test(h)) return true;
|
||||
// RFC4193 unique-local / RFC4291 link-local IPv6.
|
||||
if (h === "::1") return true;
|
||||
if (/^f[cd][0-9a-f]{2}:/i.test(h) || /^fe80:/i.test(h)) return true;
|
||||
|
||||
// IPv4 must match in FULL. Prefix checks like h.startsWith("127.") also
|
||||
// accept attacker-registrable names such as "127.0.0.1.evil.com", which
|
||||
// would silently suppress the insecure-page warning on a hostile site.
|
||||
var m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (!m) return false;
|
||||
var o = m.slice(1).map(Number);
|
||||
if (o.some(function (n) { return n > 255; })) return false;
|
||||
if (o[0] === 127) return true; // loopback
|
||||
if (o[0] === 10) return true; // RFC1918
|
||||
if (o[0] === 192 && o[1] === 168) return true; // RFC1918
|
||||
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return true; // RFC1918
|
||||
if (o[0] === 169 && o[1] === 254) return true; // link-local
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend an unmissable warning to the dropdown on plaintext public pages.
|
||||
* Deliberately a warning and not a block: filling is always user-initiated,
|
||||
* and silently offering nothing would look like a broken extension.
|
||||
*/
|
||||
function _insecureWarningRow() {
|
||||
if (_isTrustworthyOrigin()) return null;
|
||||
var row = document.createElement("div");
|
||||
Object.assign(row.style, {
|
||||
padding: "8px 12px",
|
||||
background: "#fdecea",
|
||||
color: "#b71c1c",
|
||||
borderBottom: "1px solid #f5c6cb",
|
||||
fontSize: "12px",
|
||||
lineHeight: "1.35",
|
||||
});
|
||||
row.textContent =
|
||||
"⚠ This page is not encrypted (http://). A credential filled here " +
|
||||
"can be read by anyone on the network.";
|
||||
return row;
|
||||
}
|
||||
|
||||
function visiblePasswordFields() {
|
||||
return Array.from(
|
||||
document.querySelectorAll('input[type="password"]'),
|
||||
@@ -231,24 +301,65 @@
|
||||
el.style.outline = "";
|
||||
}, 1500);
|
||||
});
|
||||
// Autologin: submit the form automatically after filling.
|
||||
// Autologin: submit automatically after filling.
|
||||
if (autologin) {
|
||||
const form = pwField.closest("form");
|
||||
if (form) {
|
||||
setTimeout(function () {
|
||||
// Prefer clicking a visible submit button so site-specific submit
|
||||
// handlers (React, Vue, etc.) fire correctly.
|
||||
var submitBtn = form.querySelector(
|
||||
'[type="submit"]:not([disabled])',
|
||||
);
|
||||
if (submitBtn) {
|
||||
submitBtn.click();
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
}, 400);
|
||||
setTimeout(function () {
|
||||
var form = pwField.closest("form");
|
||||
// Prefer clicking a real control so site-specific handlers (React, Vue,
|
||||
// inline onclick) fire — form.submit() bypasses them entirely.
|
||||
var control = _findSubmitControl(pwField);
|
||||
if (control) {
|
||||
control.click();
|
||||
} else if (form) {
|
||||
form.submit();
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Controls that look like submits but would discard the login instead.
|
||||
var _NEGATIVE_CONTROL = /cancel|reset|back|close|forgot|register|sign\s*up|create/i;
|
||||
|
||||
/**
|
||||
* Find the control that submits the login containing `pwField`.
|
||||
*
|
||||
* Autologin previously required a <form> and did nothing without one, so it
|
||||
* silently never worked on the many login UIs built from plain divs (the ASUS
|
||||
* router admin page submits with
|
||||
* <div class="button" onclick="preLogin();">Sign In</div>).
|
||||
*
|
||||
* Returns null when nothing convincing is found — better to leave the filled
|
||||
* form for the user than to click the wrong thing.
|
||||
*/
|
||||
function _findSubmitControl(pwField) {
|
||||
var scope = pwField.closest("form") || pwField.closest('[role="form"]');
|
||||
if (scope) {
|
||||
var explicit = scope.querySelector('[type="submit"]:not([disabled])');
|
||||
if (explicit && isVisible(explicit)) return explicit;
|
||||
}
|
||||
|
||||
// No form (or no explicit submit in it): search progressively wider
|
||||
// ancestors so the nearest plausible control wins.
|
||||
var node = scope || pwField.parentElement;
|
||||
for (var depth = 0; depth < 5 && node; depth++, node = node.parentElement) {
|
||||
var candidates = node.querySelectorAll(
|
||||
'button, [role="button"], [onclick], input[type="submit"], ' +
|
||||
'input[type="button"], div, a',
|
||||
);
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
var el = candidates[i];
|
||||
if (el === pwField || el.disabled) continue;
|
||||
if (!_looksLikeSubmitControl(el)) continue;
|
||||
if (!isVisible(el)) continue;
|
||||
// Only leaf-ish controls — a wrapping div can carry a button class.
|
||||
if (el.querySelector("input, button")) continue;
|
||||
var label = (el.textContent || el.value || "").trim();
|
||||
if (label.length > 40) continue;
|
||||
if (_NEGATIVE_CONTROL.test(label)) continue;
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Icon button (fixed-position, outside the DOM tree of the field) ───────────
|
||||
@@ -318,6 +429,9 @@
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
var warning = _insecureWarningRow();
|
||||
if (warning) dropdown.appendChild(warning);
|
||||
|
||||
if (panel === "more") {
|
||||
buildMorePanel(dropdown, anchorField, pwField, freshItems, filterText);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user