UNPKG

@ledgerhq/live-common

Version:
412 lines (370 loc) 16.5 kB
import { getInitialURL } from "./helpers"; describe("wallet-api helpers", () => { describe("isWhitelistedDomain (via getInitialURL)", () => { const mockManifest = { url: "https://default.example.com/", domains: ["ledger.com", "*.subdomain.ledger.com", "approved.io"], }; describe("malicious URL bypass attempts", () => { it("should reject URL with whitelisted domain in query parameter", () => { const inputs = { goToURL: "https://evil.example/?next=ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); // Should fall back to manifest.url, not use goToURL expect(result).toBe(mockManifest.url); }); it("should reject URL with whitelisted domain in hash fragment", () => { const inputs = { goToURL: "https://evil.example/#ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject URL with whitelisted domain in path", () => { const inputs = { goToURL: "https://evil.example/ledger.com/redirect", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject URL with whitelisted domain as subdomain of malicious domain", () => { const inputs = { goToURL: "https://ledger.com.evil.example", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject URL with similar but different domain", () => { const inputs = { goToURL: "https://ledger.com.fake.io", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); }); describe("scheme validation", () => { it("should reject HTTP URLs", () => { const inputs = { goToURL: "http://ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject javascript: URLs", () => { const inputs = { goToURL: "javascript:alert(1)", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject data: URLs", () => { const inputs = { goToURL: "data:text/html,<script>alert(1)</script>", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject file: URLs", () => { const inputs = { goToURL: "file:///etc/passwd", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject ftp: URLs", () => { const inputs = { goToURL: "ftp://ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); }); describe("legitimate URLs", () => { it("should reject URL not on same domain as manifest", () => { const inputs = { goToURL: "https://ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject URL with different domain even with path", () => { const inputs = { goToURL: "https://ledger.com/page?param=value", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject URL on different domain", () => { const inputs = { goToURL: "https://approved.io/some/path", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should allow URL on same domain as manifest URL", () => { const inputs = { goToURL: "https://default.example.com/different/path", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(inputs.goToURL); }); it("should allow URL on same domain as manifest URL with different path and params", () => { const inputs = { goToURL: "https://default.example.com/page?test=value", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(inputs.goToURL); }); it("should be case-insensitive for domain matching", () => { const inputs = { goToURL: "https://DEFAULT.EXAMPLE.COM/path", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(inputs.goToURL); }); it("should allow URL on same domain with different case", () => { const manifestWithUppercase = { url: "https://default.example.com/", domains: ["LEDGER.COM"], }; const inputs = { goToURL: "https://DEFAULT.EXAMPLE.COM/page", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifestWithUppercase); expect(result).toBe(inputs.goToURL); }); }); describe("wildcard pattern matching", () => { it("should reject subdomain not matching manifest domain", () => { const inputs = { goToURL: "https://test.subdomain.ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject nested subdomain not matching manifest domain", () => { const inputs = { goToURL: "https://a.b.subdomain.ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject domain not matching manifest domain", () => { const inputs = { goToURL: "https://subdomain.ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject subdomain not matching wildcard pattern", () => { const inputs = { goToURL: "https://test.wrong.ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject when wildcard pattern should not match", () => { const inputs = { goToURL: "https://notsubdomain.ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); }); describe("internationalized domain names", () => { it("should handle IDN domains (punycode) on same domain", () => { const manifestWithIDN = { url: "https://münchen.de/", domains: ["münchen.de"], }; const inputs = { goToURL: "https://xn--mnchen-3ya.de/page", // punycode for münchen.de }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifestWithIDN); expect(result).toBe(inputs.goToURL); }); it("should handle IDN domains in unicode form on same domain", () => { const manifestWithIDN = { url: "https://xn--mnchen-3ya.de/", domains: ["münchen.de"], }; const inputs = { goToURL: "https://münchen.de/page", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifestWithIDN); expect(result).toBe(inputs.goToURL); }); }); describe("edge cases", () => { it("should reject malformed URLs", () => { const inputs = { goToURL: "not-a-valid-url", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should reject empty URL", () => { const inputs = { goToURL: "", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should handle URL with port on same domain", () => { const inputs = { goToURL: "https://default.example.com:8080/path", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(inputs.goToURL); }); it("should handle URL with authentication on same domain", () => { const inputs = { goToURL: "https://user:pass@default.example.com/path", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(inputs.goToURL); }); it("should work when goToURL is not provided", () => { const inputs = {}; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); it("should work when inputs is undefined", () => { // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(undefined, mockManifest); expect(result).toBe(mockManifest.url); }); it("should handle URL with only scheme separator", () => { const inputs = { goToURL: "https://", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); }); }); describe("deeplink attack scenario", () => { it("should block deeplink with malicious goToURL", () => { // Simulates: ledgerlive://discover/<appId>?goToURL=https://evil.example/?next=ledger.com const inputs = { goToURL: "https://evil.example/?next=ledger.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(mockManifest.url); expect(result).not.toContain("evil.example"); }); it("should allow deeplink with goToURL on same domain", () => { // Simulates: ledgerlive://discover/<appId>?goToURL=https://default.example.com/page const inputs = { goToURL: "https://default.example.com/page", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, mockManifest); expect(result).toBe(inputs.goToURL); expect(result).toContain("default.example.com"); }); }); describe("real-world manifest patterns", () => { it('should handle "https://" pattern (matches nothing with strict hostname validation)', () => { const manifestWithGenericPattern = { url: "https://default.example.com/", domains: ["https://"], // This pattern doesn't make sense with hostname validation }; const inputs = { goToURL: "https://any.domain.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifestWithGenericPattern); // Should reject because "https://" is not a valid hostname pattern expect(result).toBe(manifestWithGenericPattern.url); }); it('should reject "*" pattern with non-HTTPS scheme', () => { const manifestWithHttpPattern = { url: "https://default.example.com/", domains: ["*"], // "*" is not a valid pattern }; const inputs = { goToURL: "http://any.domain.com", // HTTP should be rejected }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifestWithHttpPattern); expect(result).toBe(manifestWithHttpPattern.url); }); it('should reject "*" wildcard (all domains not allowed)', () => { const manifestWithWildcard = { url: "https://default.example.com/", domains: ["*"], }; const inputs = { goToURL: "https://any.domain.com", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifestWithWildcard); // "*" alone should not match any domain for security reasons expect(result).toBe(manifestWithWildcard.url); }); }); }); describe("getInitialURL with manifest params", () => { it("should append params when goToURL is not provided", () => { const manifest = { url: "https://example.com", domains: ["example.com"], params: { foo: "bar" }, }; const inputs = { customParam: "value" }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifest); expect(result).toContain("example.com"); expect(result).toContain("customParam=value"); expect(result).toContain("params=%7B%22foo%22%3A%22bar%22%7D"); // JSON.stringify({foo: "bar"}) encoded }); it("should use goToURL as-is when on same domain (without adding params)", () => { const manifest = { url: "https://example.com", domains: ["allowed.com"], params: { foo: "bar" }, }; const inputs = { goToURL: "https://example.com/page", customParam: "value", }; // @ts-expect-error - test mock object doesn't have all LiveAppManifest properties const result = getInitialURL(inputs, manifest); expect(result).toBe("https://example.com/page"); expect(result).not.toContain("customParam"); expect(result).not.toContain("params"); }); }); });