7cf0e6d4b0c4
fix: canary webhook filter rejects IPv4-mapped IPv6 private literals (v4.8.4)
- time
- 2026-08-04 14:36 UTC · 2 months ago
- machine
- koh2
- parent
- 2945bb7a571a
↓ peach-browser-7cf0e6d4.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.test.ts+20 −0src/background/messages.ts+22 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.8.3",4+ "version": "4.8.4",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": "4.8.3",5+ "version": "4.8.4",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": "4.8.3",3+ "version": "4.8.4",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/messages.test.ts+20 −0
@@ -719,6 +719,26 @@719719 expect(fetchMock).not.toHaveBeenCalled();720720 });721721 722+ it("F3: FIRE_CANARY_PING rejects IPv4-mapped IPv6 webhooks pointing at private ranges (Codex Security 2026-08-04)", async () => {723+ for (const webhookUrl of [724+ "https://[::ffff:192.168.0.1]/hook",725+ "https://[::ffff:c0a8:1]/hook",726+ "https://[::ffff:10.0.0.1]/hook",727+ "https://[::ffff:127.0.0.1]/hook",728+ "https://[::ffff:169.254.1.1]/hook",729+ "https://[::ffff:172.16.0.1]/hook",730+ ]) {731+ await expect(732+ dispatch({733+ type: "FIRE_CANARY_PING",734+ payload: { webhookUrl, entryTitle: "GitHub", trigger: "password_copied" },735+ })736+ ).resolves.toMatchObject({ ok: false, error: "invalid_webhook_url" });737+ }738+739+ expect(fetchMock).not.toHaveBeenCalled();740+ });741+722742 it("GET_IDENTITIES when locked returns vault locked error", async () => {723743 const vault = await import("./vault");724744 await vault.createVault("correct-pass-12345");src/background/messages.ts+22 −0
@@ -1770,6 +1770,28 @@17701770 17711771 function isPrivateIpv6(hostname: string): boolean {17721772 const normalized = hostname.replace(/^\[|\]$/g, "").toLowerCase();1773+ // IPv4-mapped IPv6 literals embed a plain IPv4 address that the prefix1774+ // list below can never match (WHATWG URL serializes [::ffff:192.168.0.1]1775+ // as ::ffff:c0a8:1). Unwrap and test the embedded IPv4; unparseable mapped1776+ // tails fail closed. (Codex Security 2026-08-04 LOW,1777+ // ssrf.webhook-url-private-ip-filter.) DNS rebinding is out of scope for a1778+ // one-shot host check.1779+ const mapped = /^::ffff:(.+)$/.exec(normalized);1780+ if (mapped) {1781+ const tail = mapped[1]!;1782+ if (tail.includes(".")) return isPrivateIpv4(tail);1783+ const parts = tail.split(":");1784+ if (parts.length !== 2 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) {1785+ return true;1786+ }1787+ const octets = [1788+ (parseInt(parts[0]!, 16) >>> 8) & 0xff,1789+ parseInt(parts[0]!, 16) & 0xff,1790+ (parseInt(parts[1]!, 16) >>> 8) & 0xff,1791+ parseInt(parts[1]!, 16) & 0xff,1792+ ];1793+ return isPrivateIpv4(octets.join("."));1794+ }17731795 return (17741796 normalized === "::1" ||17751797 normalized.startsWith("fc") ||