NoteEditor.toHtml.test.ts

3.6 KB · raw

1// @vitest-environment jsdom23import { describe, expect, it } from "vitest";4import { toHtml } from "./NoteEditor";56describe("toHtml sanitization (M15/THREAT-006)", () => {7  // Plain text path — escape special chars when no tag-like content present.8  it("escapes plain text ampersand", () => {9    expect(toHtml("a & b")).toBe("a &amp; b");10  });1112  it("converts newlines to <br> in plain text", () => {13    expect(toHtml("line1\nline2")).toBe("line1<br>line2");14  });1516  // Strips disallowed tags by replacing with their textContent.17  it("strips iframe", () => {18    expect(toHtml('<iframe src="x">payload</iframe>')).not.toContain("iframe");19  });2021  it("strips base", () => {22    expect(toHtml('<base href="//evil">')).not.toContain("base");23  });2425  it("strips meta refresh", () => {26    expect(toHtml('<meta http-equiv="refresh" content="0;url=//evil">')).not.toContain("meta");27  });2829  it("strips form", () => {30    expect(toHtml('<form action="x"><input name="y"></form>')).not.toContain("form");31  });3233  it("strips script", () => {34    expect(toHtml('<script>alert(1)</script>')).not.toContain("script");35  });3637  it("strips style", () => {38    expect(toHtml('<style>body{display:none}</style>')).not.toContain("style");39  });4041  it("strips object/embed", () => {42    expect(toHtml('<object data="x"></object>')).not.toContain("object");43    expect(toHtml('<embed src="x">')).not.toContain("embed");44  });4546  it("strips all attributes from allowed tags", () => {47    const result = toHtml('<b onclick="alert(1)" id="x" style="color:red">bold</b>');48    expect(result).toContain("<b>bold</b>");49    expect(result).not.toContain("onclick");50    expect(result).not.toContain("style");51    expect(result).not.toContain("id=");52  });5354  it("strips href from a tags (a is not in allow-list)", () => {55    const result = toHtml('<a href="//evil.com">click</a>');56    expect(result).not.toContain("href");57    expect(result).not.toContain("<a");58  });5960  // Preserve allow-listed tags.61  it("preserves bold", () => {62    expect(toHtml("<b>hi</b>")).toContain("<b>hi</b>");63  });6465  it("preserves italic, underline, strikethrough", () => {66    expect(toHtml("<i>x</i><u>y</u><s>z</s>")).toContain("<i>x</i><u>y</u><s>z</s>");67  });6869  it("preserves strong and em", () => {70    expect(toHtml("<strong>x</strong><em>y</em>")).toContain("<strong>x</strong><em>y</em>");71  });7273  it("preserves headings h2 and h3", () => {74    expect(toHtml("<h2>x</h2><h3>y</h3>")).toContain("<h2>x</h2><h3>y</h3>");75  });7677  it("preserves lists", () => {78    const result = toHtml("<ul><li>a</li><li>b</li></ul>");79    expect(result).toContain("<ul>");80    expect(result).toContain("<li>a</li>");81  });8283  it("preserves br, hr, p", () => {84    expect(toHtml("<p>x<br>y</p><hr>")).toContain("<p>");85    expect(toHtml("<p>x<br>y</p><hr>")).toContain("<br>");86    expect(toHtml("<p>x<br>y</p><hr>")).toContain("<hr>");87  });8889  // Nested adversarial cases.90  it("strips script nested inside allowed tag", () => {91    expect(toHtml("<b><script>alert(1)</script></b>")).not.toContain("script");92  });9394  it("strips javascript: URL by removing the href attribute from a-tag (a not allowed)", () => {95    const result = toHtml('<a href="javascript:alert(1)">x</a>');96    expect(result).not.toContain("javascript:");97    expect(result).not.toContain("href");98  });99100  it("strips img with onerror", () => {101    const result = toHtml('<img src="x" onerror="alert(1)">');102    expect(result).not.toContain("img");103    expect(result).not.toContain("onerror");104  });105106  it("strips svg", () => {107    expect(toHtml('<svg><script>alert(1)</script></svg>')).not.toContain("svg");108  });109});