db62c59f8b55

fix: enforce PSL-aware Firefox rpId validation with co.uk and github.io regressions (v5.0.70)

time
2026-09-03 21:52 UTC · 1 month ago
machine
koh2
agent
auto
session
PB-26-302 rpId validation
parent
af297f42b5d9
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/shared/passkey-utils.test.ts+8 −0src/shared/passkey-utils.ts+7 −24
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.69",4+  "version": "5.0.70",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "5.0.69",5+  "version": "5.0.70",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.0.69",3+  "version": "5.0.70",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/shared/passkey-utils.test.ts+8 −0
@@ -19,6 +19,14 @@1919     expect(isValidRpId("google.com", "https://evil.google.com.attacker.com")).toBe(false);2020   });2121 22+  it("rejects a public-suffix rpId for co.uk tenants", () => {23+    expect(isValidRpId("co.uk", "https://evil.co.uk")).toBe(false);24+  });25+26+  it("rejects a public-suffix rpId for github.io tenants", () => {27+    expect(isValidRpId("github.io", "https://attacker.github.io")).toBe(false);28+  });29+2230   it("rejects a bare-eTLD rpId", () => {2331     expect(isValidRpId("com", "https://google.com")).toBe(false);2432   });

view file

src/shared/passkey-utils.ts+7 −24
@@ -9,21 +9,13 @@99  * happily mint) a credential scoped to `google.com` (SEV-1-A).1010  */1111 12+import { parse } from "tldts";13+1214 /**1315  * Validates that `rpId` is equal to, or a registrable-domain-suffix of, the1416  * hostname of `callerOrigin`. Mirrors WebAuthn §7.1 step 6 / §7.2 step 8.15- *16- * This is a conservative suffix check, not a full Public-Suffix-List lookup:17- * it requires the rpId to contain at least one dot (blocking bare-eTLD rpIds18- * like "com" or "localhost"), and requires the caller host to be exactly the19- * rpId or to end with "." + rpId. That correctly rejects the classic20- * suffix-confusion payload "google.com.attacker.com" for rpId "google.com".21- *22- *   isValidRpId("google.com", "https://accounts.google.com") → true23- *   isValidRpId("google.com", "https://google.com")          → true24- *   isValidRpId("com",        "https://google.com")          → false (bare eTLD)25- *   isValidRpId("google.com", "https://evil.google.com.x.com")→ false26- *   isValidRpId("evil.com",   "https://google.com")          → false17+ * Private suffixes are enabled because services such as github.io host18+ * unrelated tenants beneath the same public suffix.2719  */2820 export function isValidRpId(rpId: string, callerOrigin: string): boolean {2921   let callerHost: string;@@ -36,18 +28,9 @@3628 3729   const id = rpId.toLowerCase();3830   const host = callerHost.toLowerCase();39-40-  // Require at least one dot — blocks bare public suffixes ("com", "co.uk"41-  // collapses are not handled here, but a bare TLD is the dangerous case).4231   if (!id.includes(".")) return false;4332 44-  // Exact match.45-  if (host === id) return true;46-47-  // Registrable-suffix match: host must end with ".<rpId>". This rejects48-  // "google.com.attacker.com" for rpId "google.com" because that host ends49-  // with ".attacker.com", not ".google.com".50-  if (host.endsWith("." + id)) return true;51-52-  return false;33+  const rp = parse(id, { allowPrivateDomains: true });34+  const caller = parse(host, { allowPrivateDomains: true });35+  return rp.domain === id && caller.domain === rp.domain;5336 }

view file