|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { |
| 3 | + applyCspToHtml, |
| 4 | + buildCspMetaTag, |
| 5 | + buildCspString, |
| 6 | + escapeAttr, |
| 7 | + sanitizeDomain, |
| 8 | +} from "./mcpAppCsp"; |
| 9 | + |
| 10 | +describe("sanitizeDomain", () => { |
| 11 | + it.each([ |
| 12 | + ["example.com", "example.com"], |
| 13 | + ["*.example.com", "*.example.com"], |
| 14 | + ["example.com:8080", "example.com:8080"], |
| 15 | + ["' unsafe-eval; script-src *;", "unsafe-evalscript-src*"], |
| 16 | + ['" onload=alert(1)', "onloadalert1"], |
| 17 | + ["example.com; frame-ancestors *", "example.comframe-ancestors*"], |
| 18 | + ["example .com", "example.com"], |
| 19 | + ])("sanitizes %j", (input, expected) => { |
| 20 | + expect(sanitizeDomain(input)).toBe(expected); |
| 21 | + }); |
| 22 | +}); |
| 23 | + |
| 24 | +describe("buildCspString", () => { |
| 25 | + it.each([ |
| 26 | + ["default-src 'none'"], |
| 27 | + ["script-src 'self' 'unsafe-inline'"], |
| 28 | + ["style-src 'self' 'unsafe-inline'"], |
| 29 | + ["img-src 'self' data:"], |
| 30 | + ["media-src 'self' data:"], |
| 31 | + ["connect-src 'none'"], |
| 32 | + ["frame-src 'none'"], |
| 33 | + ["form-action 'none'"], |
| 34 | + ["base-uri 'none'"], |
| 35 | + ["object-src 'none'"], |
| 36 | + ])("default policy contains %s", (directive) => { |
| 37 | + expect(buildCspString()).toContain(directive); |
| 38 | + }); |
| 39 | + |
| 40 | + it.each([ |
| 41 | + ["connect-src 'none'"], |
| 42 | + ["frame-src 'none'"], |
| 43 | + ["form-action 'none'"], |
| 44 | + ["base-uri 'none'"], |
| 45 | + ["img-src 'self' data:"], |
| 46 | + ])("uses the restrictive default %s for empty metadata", (directive) => { |
| 47 | + expect(buildCspString({})).toContain(directive); |
| 48 | + }); |
| 49 | + |
| 50 | + it("maps connectDomains to connect-src", () => { |
| 51 | + expect( |
| 52 | + buildCspString({ |
| 53 | + connectDomains: ["api.example.com", "*.cdn.example.com"], |
| 54 | + }), |
| 55 | + ).toContain("connect-src api.example.com *.cdn.example.com"); |
| 56 | + }); |
| 57 | + |
| 58 | + it("maps resourceDomains to img/media/font/script/style-src", () => { |
| 59 | + const result = buildCspString({ resourceDomains: ["cdn.example.com"] }); |
| 60 | + expect(result).toContain("img-src 'self' data: cdn.example.com"); |
| 61 | + expect(result).toContain("media-src 'self' data: cdn.example.com"); |
| 62 | + expect(result).toContain("font-src cdn.example.com"); |
| 63 | + expect(result).toContain( |
| 64 | + "script-src 'self' 'unsafe-inline' cdn.example.com", |
| 65 | + ); |
| 66 | + expect(result).toContain( |
| 67 | + "style-src 'self' 'unsafe-inline' cdn.example.com", |
| 68 | + ); |
| 69 | + }); |
| 70 | + |
| 71 | + it("omits resourceDomains from script/style-src when not declared", () => { |
| 72 | + const result = buildCspString({}); |
| 73 | + expect(result).toContain("script-src 'self' 'unsafe-inline'"); |
| 74 | + expect(result).not.toMatch(/script-src 'self' 'unsafe-inline' ;/); |
| 75 | + }); |
| 76 | + |
| 77 | + it("maps frameDomains to frame-src", () => { |
| 78 | + expect(buildCspString({ frameDomains: ["embed.example.com"] })).toContain( |
| 79 | + "frame-src embed.example.com", |
| 80 | + ); |
| 81 | + }); |
| 82 | + |
| 83 | + it("maps baseUriDomains to base-uri", () => { |
| 84 | + expect(buildCspString({ baseUriDomains: ["example.com"] })).toContain( |
| 85 | + "base-uri example.com", |
| 86 | + ); |
| 87 | + }); |
| 88 | + |
| 89 | + it("always includes form-action 'none'", () => { |
| 90 | + expect(buildCspString({ connectDomains: ["api.example.com"] })).toContain( |
| 91 | + "form-action 'none'", |
| 92 | + ); |
| 93 | + }); |
| 94 | + |
| 95 | + it("sanitizes injection attempts in domains", () => { |
| 96 | + const result = buildCspString({ |
| 97 | + connectDomains: ["example.com; script-src 'unsafe-eval'"], |
| 98 | + }); |
| 99 | + expect(result).toContain("connect-src example.comscript-srcunsafe-eval"); |
| 100 | + expect(result).not.toMatch(/;\s*script-src\s+'unsafe-eval'/); |
| 101 | + }); |
| 102 | +}); |
| 103 | + |
| 104 | +describe("escapeAttr", () => { |
| 105 | + it.each([ |
| 106 | + ['hello "world"', "hello "world""], |
| 107 | + ["hello 'world'", "hello 'world'"], |
| 108 | + ["a & b", "a & b"], |
| 109 | + ["<script>alert(1)</script>", "<script>alert(1)</script>"], |
| 110 | + ["default-src none", "default-src none"], |
| 111 | + ])("escapes %j", (input, expected) => { |
| 112 | + expect(escapeAttr(input)).toBe(expected); |
| 113 | + }); |
| 114 | +}); |
| 115 | + |
| 116 | +describe("buildCspMetaTag", () => { |
| 117 | + it("returns a valid meta tag with the default policy", () => { |
| 118 | + const tag = buildCspMetaTag(); |
| 119 | + expect(tag).toMatch( |
| 120 | + /^<meta http-equiv="Content-Security-Policy" content=".*">$/, |
| 121 | + ); |
| 122 | + expect(tag).toContain("default-src"); |
| 123 | + }); |
| 124 | + |
| 125 | + it("escapes the CSP content in the attribute", () => { |
| 126 | + const tag = buildCspMetaTag({ connectDomains: ["example.com"] }); |
| 127 | + expect(tag).toContain("connect-src example.com"); |
| 128 | + expect(tag).toMatch(/content="[^"]+"/); |
| 129 | + }); |
| 130 | +}); |
| 131 | + |
| 132 | +describe("applyCspToHtml", () => { |
| 133 | + it("prepends the CSP meta when there is no doctype", () => { |
| 134 | + const out = applyCspToHtml("<html><body>hi</body></html>"); |
| 135 | + expect(out.startsWith(buildCspMetaTag())).toBe(true); |
| 136 | + }); |
| 137 | + |
| 138 | + it("inserts the CSP meta after a leading doctype", () => { |
| 139 | + const out = applyCspToHtml("<!doctype html><html><head></head></html>"); |
| 140 | + expect(out).toBe( |
| 141 | + `<!doctype html>${buildCspMetaTag()}<html><head></head></html>`, |
| 142 | + ); |
| 143 | + }); |
| 144 | + |
| 145 | + it("keeps leading whitespace and mixed-case doctype before the meta", () => { |
| 146 | + const out = applyCspToHtml(" <!DOCTYPE html>\n<html></html>"); |
| 147 | + expect(out.startsWith(" <!DOCTYPE html>")).toBe(true); |
| 148 | + expect(out.indexOf("<!DOCTYPE html>")).toBeLessThan( |
| 149 | + out.indexOf(buildCspMetaTag()), |
| 150 | + ); |
| 151 | + }); |
| 152 | +}); |
0 commit comments