diff --git a/apps/extension/README.md b/apps/extension/README.md index 7b7c3aa..3b9b005 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -14,3 +14,21 @@ Build-time public env (WXT inlines `WXT_PUBLIC_*` into the client bundle): - A **dev build** (`wxt dev`) with this unset falls back to `http://localhost:3000` and `http://localhost:5173`. - `WXT_PUBLIC_ALLOW_ANY_PAIR_ORIGIN` — set to `1` to explicitly disable the pair-origin restriction (any origin may pair). Named escape hatch only; logs a warning on every startup. **Never set this in a production build.** - `WXT_PUBLIC_MAINNET_RPC_URL` — trusted Soroban RPC used to anchor the device signer's signature-expiration ledger on **mainnet** (L4). The extension never trusts the paired wallet's `rpcUrl` for this. Testnet uses SDF's pinned public endpoint automatically; mainnet has no universal public RPC, so this must be set — **mainnet signing fails closed if it is unset** rather than trusting the caller-supplied endpoint. + +## Input Sanitization (#312) + +dApp-provided connection payloads (such as dApp names, origins, descriptions, and icon URLs) are sanitized before being processed or rendered in the popup UI: +- **HTML & Script Escaping**: HTML special characters (`<`, `>`, `&`, `"`, `'`, `/`) are escaped using `escapeHtml()`. +- **Tag Stripping**: All HTML tags (`")).toBe( + "<script>alert('xss')</script>", + ); + expect(escapeHtml('Hello "World" & ')).toBe( + "Hello "World" & <Friends>", + ); + }); + }); + + describe("sanitizeUrl", () => { + it("blocks javascript: and data: URIs", () => { + expect(sanitizeUrl("javascript:alert(1)")).toBe(""); + expect(sanitizeUrl("JAVASCRIPT:alert(1)")).toBe(""); + expect(sanitizeUrl("data:text/html,")).toBe(""); + expect(sanitizeUrl("vbscript:msgbox(1)")).toBe(""); + }); + + it("allows valid http and https URLs", () => { + expect(sanitizeUrl("https://example.com/icon.png")).toBe( + "https://example.com/icon.png", + ); + }); + + it("handles undefined or null inputs", () => { + expect(sanitizeUrl(undefined)).toBe(""); + expect(sanitizeUrl("")).toBe(""); + }); + }); + + describe("sanitizeString", () => { + it("strips HTML tags and removes control characters", () => { + expect(sanitizeString("Malicious")).toBe( + "Malicious", + ); + expect(sanitizeString("Clean\x00Name")).toBe("CleanName"); + }); + + it("truncates string to specified max length", () => { + const longInput = "a".repeat(200); + expect(sanitizeString(longInput, 50).length).toBe(50); + }); + + it("handles non-string or malformed inputs safely", () => { + expect(sanitizeString(12345)).toBe("12345"); + expect(sanitizeString({ invalid: "object" })).toBe("[object Object]"); + expect(sanitizeString(null)).toBe(""); + expect(sanitizeString(undefined)).toBe(""); + }); + }); + + describe("sanitizeDAppMetadata", () => { + it("sanitizes full dApp metadata payload containing script injection attempts", () => { + const payload = { + name: "DApp Exchange", + description: "Best DEX for tokens", + iconUrl: "javascript:void(0)", + origin: "https://dapp.example.com", + }; + + const result = sanitizeDAppMetadata(payload); + + expect(result.name).not.toContain("