8c8a5b1478bc

fix: LV-2 detectHomograph flags non-ASCII and xn-- punycode hostnames (non-blocking detector; edit/save-banner UI lands in UI-8)

time
2026-06-10 02:38 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
audit fix integration
parent
878cf96cfacb
2 files src/shared/domain.homograph.test.ts+27 −0src/shared/domain.ts+21 −0
src/shared/domain.homograph.test.ts+27 −0
@@ -0,0 +1,27 @@1+// LV-2: homograph detector tests.2+import { describe, expect, it } from "vitest";3+import { detectHomograph } from "./domain";4+5+describe("detectHomograph", () => {6+  it("flags a hostname with non-ASCII (Cyrillic homograph) characters", () => {7+    // "аpple.com" — leading а is Cyrillic U+0430, not Latin a.8+    expect(detectHomograph("https://аpple.com")).toBe(true);9+  });10+11+  it("flags a punycode (xn--) label", () => {12+    expect(detectHomograph("https://xn--pple-43d.com")).toBe(true);13+  });14+15+  it("accepts a bare hostname without a scheme", () => {16+    expect(detectHomograph("xn--pple-43d.com")).toBe(true);17+  });18+19+  it("returns false for a plain ASCII domain", () => {20+    expect(detectHomograph("https://apple.com")).toBe(false);21+  });22+23+  it("returns false for malformed input", () => {24+    expect(detectHomograph("")).toBe(false);25+    expect(detectHomograph("http://")).toBe(false);26+  });27+});

view file

src/shared/domain.ts+21 −0
@@ -26,3 +26,24 @@2626     return url;2727   }2828 }29+30+/**31+ * LV-2: true when the URL's hostname contains non-ASCII characters — a32+ * potential homograph (Cyrillic "аpple.com" etc.) — OR a punycode (xn--)33+ * label, which is the URL parser's normalized form of the same risk.34+ * Matching/keying is NOT affected (tldts keeps Unicode labels distinct,35+ * preventing cross-matching); this only drives a non-blocking UI warning so a36+ * user who saved or imported such a URL can see it isn't the ASCII site.37+ * Accepts bare hostnames as well as full URLs.38+ */39+export function detectHomograph(url: string): boolean {40+  try {41+    const hostname = new URL(url.includes("://") ? url : `https://${url}`).hostname;42+    return (43+      /[^\x00-\x7F]/.test(hostname) ||44+      hostname.split(".").some((l) => l.startsWith("xn--"))45+    );46+  } catch {47+    return false;48+  }49+}

view file