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
↓ peach-browser-db62c59f.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/shared/passkey-utils.test.ts+8 −0src/shared/passkey-utils.ts+7 −24manifest.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": {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",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",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 });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 }