Aug 26 - Enhance security 3
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:
2026-08-26 13:54:48 -04:00
parent 6c1bef73c8
commit cc216b0d98
16 changed files with 11056 additions and 43 deletions
+10 -3
View File
@@ -113,14 +113,21 @@ async function updateBadgeForTab(tabId, url) {
return;
}
let hostname;
try { hostname = new URL(url).hostname.replace(/^www\./, ''); } catch {
try { hostname = new URL(url).hostname; } catch {
chrome.browserAction.setBadgeText({ text: '', tabId }); return;
}
// Registrable-domain comparison via the vendored PSL (loaded ahead of this
// file by manifest.firefox.json background.scripts), matching content.js
// and popup.js. Falls back to exact equality so a load failure undercounts
// rather than counting an attacker's neighbouring subdomain.
const sameSite =
typeof PkPsl !== 'undefined' && PkPsl?.isSameSite
? PkPsl.isSameSite
: (a, b) => String(a).toLowerCase() === String(b).toLowerCase();
const matches = vault_items.filter((item) => {
if (item.item_type !== 'password' || !item.plain?.url) return false;
try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
return h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`);
return sameSite(new URL(item.plain.url).hostname, hostname);
} catch { return false; }
});
if (matches.length > 0) {
+16 -7
View File
@@ -8,6 +8,12 @@
* - Lock the vault automatically after IDLE_LOCK_SECONDS of system inactivity.
*/
// Public Suffix List — the badge counts matching items, and must use the same
// same-site rule as content.js and popup.js. Counting a match on an attacker's
// neighbouring subdomain is itself a misleading signal, even though the badge
// alone does not disclose a credential.
importScripts("shared/psl.js");
// ── Idle lock ─────────────────────────────────────────────────────────────────
// Default: never lock (session clears naturally on browser close via chrome.storage.session).
@@ -119,21 +125,24 @@ async function updateBadgeForTab(tabId, url) {
let hostname;
try {
hostname = new URL(url).hostname.replace(/^www\./, "");
hostname = new URL(url).hostname;
} catch {
chrome.action.setBadgeText({ text: "", tabId });
return;
}
// Registrable-domain comparison, matching content.js and popup.js. Falls
// back to exact equality if psl.js is unavailable — strict, so a load
// failure undercounts rather than counting an attacker's subdomain.
const sameSite =
typeof PkPsl !== "undefined" && PkPsl?.isSameSite
? PkPsl.isSameSite
: (a, b) => String(a).toLowerCase() === String(b).toLowerCase();
const matches = vault_items.filter((item) => {
if (item.item_type !== "password" || !item.plain?.url) return false;
try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, "");
return (
h === hostname ||
h.endsWith(`.${hostname}`) ||
hostname.endsWith(`.${h}`)
);
return sameSite(new URL(item.plain.url).hostname, hostname);
} catch {
return false;
}
+26 -4
View File
@@ -922,17 +922,39 @@
return "https://" + s; // bare domain or path
}
/**
* Select the stored items whose URL belongs to the page we are on.
*
* Matching is by registrable domain (PkPsl.isSameSite), NOT by suffix
* comparison. The previous test was:
*
* h === host || h.endsWith("." + host) || host.endsWith("." + h)
*
* which had no notion of a public suffix, so a credential saved for
* victim.github.io was offered on evil.github.io, and one saved for a bare
* TLD was offered everywhere under it. Surfacing a match on an attacker's
* neighbouring subdomain defeats the phishing resistance that is most of the
* point of a password manager.
*
* If psl.js somehow failed to load we fall back to exact hostname equality —
* strict, so a load failure loses matches rather than leaking credentials.
*/
function _filterForHost(items) {
var host = location.hostname.replace(/^www\./, "");
var host = location.hostname;
var sameSite =
typeof PkPsl !== "undefined" && PkPsl && PkPsl.isSameSite
? PkPsl.isSameSite
: function (a, b) {
return String(a).toLowerCase() === String(b).toLowerCase();
};
return (items || []).filter(function (item) {
if (item.item_type !== "password" || !(item.plain && item.plain.url))
return false;
try {
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);
return sameSite(new URL(normalised).hostname, host);
} catch (e) {
console.warn(
"[PassKeeper] _filterForHost: could not parse URL:",
+20 -5
View File
@@ -21,18 +21,33 @@
}
},
"background": {
"scripts": ["background.firefox.js"],
"scripts": [
"shared/psl.js",
"background.firefox.js"
],
"persistent": false
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["shared/browser-polyfill.js", "content/content.js"],
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"shared/browser-polyfill.js",
"shared/psl.js",
"content/content.js"
],
"run_at": "document_idle"
},
{
"matches": ["https://pwkeeper.ngodanguyen.tech/*"],
"js": ["shared/browser-polyfill.js", "bridge/bridge.js"],
"matches": [
"https://pwkeeper.ngodanguyen.tech/*"
],
"js": [
"shared/browser-polyfill.js",
"bridge/bridge.js"
],
"run_at": "document_idle"
}
],
+5 -2
View File
@@ -32,6 +32,7 @@
"https://*/*"
],
"js": [
"shared/psl.js",
"content/content.js"
],
"run_at": "document_idle"
@@ -49,7 +50,9 @@
"web_accessible_resources": [
{
"resources": [],
"matches": ["<all_urls>"]
"matches": [
"<all_urls>"
]
}
],
"icons": {
@@ -66,4 +69,4 @@
"description": "Open PassKeeper"
}
}
}
}
+3
View File
@@ -621,6 +621,9 @@
</nav>
</div>
<!-- /app -->
<!-- Public Suffix List — must load before popup.js, which calls PkPsl
from isMatch() to decide which stored items belong to the active tab. -->
<script src="../shared/psl.js"></script>
<script src="../shared/crypto.js"></script>
<script src="../shared/sharing-crypto.js"></script>
<script src="popup.js"></script>
+16 -3
View File
@@ -220,15 +220,28 @@ function _normaliseUrl(raw) {
return "https://" + s;
}
/**
* Does this stored item belong to the site in the active tab?
*
* Compared by registrable domain (PkPsl.isSameSite), not by suffix. The old
* test had no notion of a public suffix, so victim.github.io matched
* evil.github.io and an item saved for a bare TLD matched every site under it.
* Must stay in step with _filterForHost() in content/content.js.
*
* Falls back to exact hostname equality if psl.js failed to load strict, so a
* load failure loses matches rather than leaking credentials.
*/
function isMatch(item) {
const host = currentHostname();
if (!host || item.item_type !== "password" || !item.plain?.url) return false;
try {
const normalised = _normaliseUrl(item.plain.url);
if (!normalised) return false;
const h = new URL(normalised).hostname.replace(/^www\./, "");
// Match exact domain or any subdomain relationship.
return h === host || h.endsWith(`.${host}`) || host.endsWith(`.${h}`);
const itemHost = new URL(normalised).hostname;
if (typeof PkPsl !== "undefined" && PkPsl?.isSameSite) {
return PkPsl.isSameSite(itemHost, host);
}
return itemHost.toLowerCase() === String(host).toLowerCase();
} catch {
return false;
}
File diff suppressed because it is too large Load Diff