Skip to content
File

Blob: tests/client/email-html/url-policy.test.ts

typescript76 lines
1import { describe, expect, it } from "vitest";
2import {
3 isAllowedInlineResourceUrl,
4 isAllowedNavigationUrl,
5 isRemoteResourceUrl,
6 isUnsafeUrl,
7 stripQuotes,
8} from "@/client/lib/email-html/url-policy";
9 
10describe("stripQuotes", () => {
11 it("trims surrounding whitespace and quotes", () => {
12 expect(stripQuotes(" 'https://example.com/path' ")).toBe("https://example.com/path");
13 expect(stripQuotes(' "cid:image" ')).toBe("cid:image");
14 });
15});
16 
17describe("isUnsafeUrl", () => {
18 it("rejects dangerous protocols and executable data html", () => {
19 expect(isUnsafeUrl("javascript:alert(1)")).toBe(true);
20 expect(isUnsafeUrl("vbscript:msgbox(1)")).toBe(true);
21 expect(isUnsafeUrl("file:///tmp/secret.txt")).toBe(true);
22 expect(isUnsafeUrl("data:text/html,<script>alert(1)</script>")).toBe(true);
23 expect(isUnsafeUrl("data:application/xhtml+xml,<html></html>")).toBe(true);
24 });
25 
26 it("does not treat safe resource urls as unsafe", () => {
27 expect(isUnsafeUrl("https://example.com/asset.png")).toBe(false);
28 expect(isUnsafeUrl("cid:inline-image")).toBe(false);
29 });
30});
31 
32describe("isAllowedInlineResourceUrl", () => {
33 it("accepts inline-safe resource schemes", () => {
34 expect(isAllowedInlineResourceUrl("data:image/png;base64,abc123")).toBe(true);
35 expect(isAllowedInlineResourceUrl("cid:inline-image")).toBe(true);
36 expect(isAllowedInlineResourceUrl("about:blank")).toBe(true);
37 expect(isAllowedInlineResourceUrl("blob:https://example.com/123")).toBe(true);
38 });
39 
40 it("rejects remote navigation urls", () => {
41 expect(isAllowedInlineResourceUrl("https://example.com/asset.png")).toBe(false);
42 expect(isAllowedInlineResourceUrl("mailto:test@example.com")).toBe(false);
43 });
44});
45 
46describe("isRemoteResourceUrl", () => {
47 it("accepts http, https, and protocol-relative urls", () => {
48 expect(isRemoteResourceUrl("https://example.com/image.png")).toBe(true);
49 expect(isRemoteResourceUrl("http://example.com/image.png")).toBe(true);
50 expect(isRemoteResourceUrl("//cdn.example.com/image.png")).toBe(true);
51 });
52 
53 it("rejects inline and non-remote urls", () => {
54 expect(isRemoteResourceUrl("cid:inline-image")).toBe(false);
55 expect(isRemoteResourceUrl("/relative/path.png")).toBe(false);
56 expect(isRemoteResourceUrl("#section")).toBe(false);
57 });
58});
59 
60describe("isAllowedNavigationUrl", () => {
61 it("allows empty, fragment, remote, mailto, and tel navigation", () => {
62 expect(isAllowedNavigationUrl("")).toBe(true);
63 expect(isAllowedNavigationUrl("#details")).toBe(true);
64 expect(isAllowedNavigationUrl("https://example.com/path")).toBe(true);
65 expect(isAllowedNavigationUrl("//cdn.example.com/path")).toBe(true);
66 expect(isAllowedNavigationUrl("mailto:test@example.com")).toBe(true);
67 expect(isAllowedNavigationUrl("tel:+15555550123")).toBe(true);
68 });
69 
70 it("rejects unsupported or unsafe navigation targets", () => {
71 expect(isAllowedNavigationUrl("file:///tmp/secret.txt")).toBe(false);
72 expect(isAllowedNavigationUrl("javascript:alert(1)")).toBe(false);
73 expect(isAllowedNavigationUrl("/internal/path")).toBe(false);
74 });
75});