import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import { safeImageUrl, safeUrl } from "./safe-url.ts"; describe("safe-url", () => { describe("safeUrl()", () => { it("returns an `https` URL unchanged", () => { expect(safeUrl("https://example.com/a?b=c#d")).toBe( "https://example.com/a?b=c#d", ); }); it("returns a `mailto` URL unchanged", () => { expect(safeUrl("mailto:someone@example.com")).toBe( "mailto:someone@example.com", ); }); it("returns a relative URL unchanged", () => { expect(safeUrl("/docs/guide.md")).toBe("/docs/guide.md"); expect(safeUrl("../sibling.png")).toBe("../sibling.png"); }); it("returns a fragment unchanged", () => { expect(safeUrl("#section-one")).toBe("#section-one"); }); it("returns a relative path whose later segment contains a colon", () => { // The colon is not a scheme separator here, because what precedes it is // not a scheme: a path segment intervenes. expect(safeUrl("path/to:file")).toBe("path/to:file"); expect(safeUrl("/of:bafy123/field")).toBe("/of:bafy123/field"); }); it("returns `null` for a `javascript` URL", () => { expect(safeUrl("javascript:alert(1)")).toBeNull(); }); it("returns `null` for a `javascript` URL in any casing", () => { expect(safeUrl("JaVaScRiPt:alert(1)")).toBeNull(); }); it("returns `null` for a scheme split or padded by whitespace", () => { // The URL parser discards these before it reads the scheme, so each of // these names the `javascript` scheme to a browser. expect(safeUrl("java\tscript:alert(1)")).toBeNull(); expect(safeUrl("java\nscript:alert(1)")).toBeNull(); expect(safeUrl("java\rscript:alert(1)")).toBeNull(); expect(safeUrl(" javascript:alert(1)")).toBeNull(); }); it("returns `null` for a `data` URL", () => { expect(safeUrl("data:text/html,")).toBeNull(); expect(safeUrl("data:image/png;base64,AAAA")).toBeNull(); }); it("returns `null` for a scheme outside the allowlist", () => { expect(safeUrl("vbscript:msgbox(1)")).toBeNull(); expect(safeUrl("file:///etc/passwd")).toBeNull(); expect(safeUrl("blob:https://example.com/abc")).toBeNull(); }); }); describe("safeImageUrl()", () => { it("returns an `https` URL unchanged", () => { expect(safeImageUrl("https://example.com/a.png")).toBe( "https://example.com/a.png", ); }); it("returns a `data:image` URL unchanged", () => { expect(safeImageUrl("data:image/png;base64,AAAA")).toBe( "data:image/png;base64,AAAA", ); expect(safeImageUrl("DATA:IMAGE/SVG+XML,%3Csvg%3E")).toBe( "DATA:IMAGE/SVG+XML,%3Csvg%3E", ); }); it("returns `null` for a `data` URL that is not an image", () => { expect(safeImageUrl("data:text/html,")) .toBeNull(); }); it("returns `null` for a `javascript` URL", () => { expect(safeImageUrl("javascript:alert(1)")).toBeNull(); }); it("reads the scheme past a control character in a `data:image` URL", () => { expect(safeImageUrl("data\t:image/png;base64,AAAA")).toBe( "data\t:image/png;base64,AAAA", ); }); }); });