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
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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+10374
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user