-
Notifications
You must be signed in to change notification settings - Fork 114
Apply mXSS-safe SAFE_FOR_XML on attachment content re-inflation #1226
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
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
868196c
Apply mXSS-safe SAFE_FOR_XML on attachment content re-inflation
jeremy 41a429d
Cover the attachment content round trip with a system test
jeremy 144f0e6
Neutralize with a separator, so removals cannot merge
jeremy 4019e0e
Assert what re-inflation actually leaves in the editor
jeremy 214e8da
Drop the content preservation hook — it guards a shape production nev…
jeremy 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
52 changes: 52 additions & 0 deletions
52
test/javascript/unit/editor/attachments/content_reinflation_sanitization.test.js
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,52 @@ | ||
| import { afterEach, describe, expect, test } from "vitest" | ||
| import { createTestEditor, destroyTestEditor, tick } from "../../helpers/editor_helper" | ||
|
|
||
| let editorElement | ||
|
|
||
| afterEach(async () => { | ||
| await destroyTestEditor(editorElement) | ||
| }) | ||
|
|
||
| // Re-inflating stored attachment content is an untrusted storage round-trip. The | ||
| // custom attachment node re-parses the serialized `content` HTML into the live | ||
| // editor DOM, so it must be sanitized in DOMPurify's mXSS-safe mode while keeping | ||
| // legitimate content (including comments) intact. | ||
| describe("attachment content re-inflation sanitization", () => { | ||
| const attachment = (content) => | ||
| `<action-text-attachment content-type="text/html" sgid="abc123" content="${content}"></action-text-attachment>` | ||
|
|
||
| // The payload has to be one SAFE_FOR_XML actually decides, or the test passes | ||
| // with the change reverted and proves nothing. An ordinary `onerror` doesn't | ||
| // qualify: DOMPurify's tag and attribute allowlists remove it either way. | ||
| // | ||
| // A comment terminator inside an attribute value does. SAFE_FOR_XML drops any | ||
| // attribute whose value could close a comment or a raw-text element when the | ||
| // sanitized markup is re-serialized and parsed again — which is exactly what | ||
| // re-inflating stored content does. | ||
| test("drops an attribute whose value can break out of a comment", async () => { | ||
| const payload = "<p title="--><img src=x onerror=alert(1)>">hi</p>" | ||
|
jeremy marked this conversation as resolved.
|
||
| editorElement = await createTestEditor({ value: attachment(payload) }) | ||
| await tick() | ||
|
|
||
| const figure = editorElement.querySelector("action-text-attachment, [content-type]") | ||
| expect(figure, "attachment was dropped on re-inflation").not.toBeNull() | ||
|
|
||
| // Verified by reverting the safeForXml opt-in in createDOM: without it the | ||
| // title survives verbatim and this assertion fails. | ||
| expect(figure.innerHTML).not.toContain("-->") | ||
| expect(figure.innerHTML).not.toMatch(/onerror/i) | ||
| expect(figure.textContent).toContain("hi") | ||
| }) | ||
|
|
||
| test("preserves legitimate comment-bearing attachment content after round-trip", async () => { | ||
| const content = "<!-- BEGIN app/views/users/_user.html.erb --><span>Chris</span><!-- END app/views/users/_user.html.erb -->" | ||
| editorElement = await createTestEditor({ value: attachment(content) }) | ||
|
jeremy marked this conversation as resolved.
jeremy marked this conversation as resolved.
jeremy marked this conversation as resolved.
|
||
| await tick() | ||
|
|
||
| // The attachment survives, and its exported value still carries the content. | ||
| expect(editorElement.value).toContain("action-text-attachment") | ||
| expect(editorElement.value).toContain("BEGIN app/views/users/_user.html.erb") | ||
| // The rendered inner content is present in the editor DOM. | ||
| expect(editorElement.textContent).toContain("Chris") | ||
| }) | ||
| }) | ||
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,73 @@ | ||
| require "application_system_test_case" | ||
|
|
||
| # Attachment content is re-sanitized in mXSS-safe mode every time the attachment | ||
| # renders in the editor, so comment-bearing content — Rails view annotations land | ||
| # inside the partial an attachment renders — has to survive the whole loop: the | ||
| # editor, the saved value, the rendered page and the re-edited document all have to | ||
| # agree, with Loofah on the server getting a say between them. Unit tests only ever | ||
| # see the first hop; this is the test that would catch a server stage dropping it. | ||
| # | ||
| # The content survives without any special force-keep. The serialized `content` | ||
| # attribute is only produced by exportDOM, where SAFE_FOR_XML is off, so it is never | ||
| # subject to the mXSS guard; the safe-XML call in CustomActionTextAttachmentNode#createDOM | ||
| # is handed the *decoded inner* markup, which has no `content` attribute of its own. | ||
| # The client-side re-inflation guard is covered directly in | ||
| # test/javascript/unit/editor/attachments/content_reinflation_sanitization.test.js. | ||
| class AttachmentContentRoundTripTest < ApplicationSystemTestCase | ||
| COMMENT = "BEGIN app/views/people/_person.html.erb" | ||
|
|
||
| test "comment-bearing attachment content survives save, render and re-edit" do | ||
| person = people(:james) | ||
|
|
||
| visit edit_post_path(posts(:hello_james)) | ||
|
jeremy marked this conversation as resolved.
|
||
| wait_for_editor | ||
|
|
||
| assert_comment_in_saved_value | ||
| assert_mention_in_editor person | ||
|
|
||
| click_on "Update Post" | ||
|
|
||
| # The rendered page: the attachment has been through Loofah on the way in. | ||
| within "article.post" do | ||
| assert_selector %(bc-mention[gid="#{person.to_gid}"]), text: person.name | ||
| end | ||
|
|
||
| click_on "Edit this post" | ||
| wait_for_editor | ||
|
|
||
| # The re-edit: content is re-inflated under SAFE_FOR_XML, and the attachment | ||
| # renders from it. Lose the content on any hop and the attachment comes back | ||
| # empty here. | ||
| assert_mention_in_editor person | ||
| assert_comment_in_saved_value | ||
|
jeremy marked this conversation as resolved.
|
||
| end | ||
|
|
||
| private | ||
| # Asserted on the attachment element rather than on bc-mention, which never | ||
| # reaches the editor DOM: an editor's allowlist is its importable tags plus | ||
| # whatever its extensions declare, and the dummy app declares no | ||
| # allowedElements for bc-mention. DOMPurify drops an unlisted tag and keeps | ||
| # its children, so what re-inflation leaves behind is the rendered mention — | ||
| # the avatar and the name — inside the attachment. That is the property this | ||
| # test is after: lose the content and the attachment renders empty. | ||
| # | ||
| # The gid is not dropped, only relocated: mention_round_trip_test asserts it | ||
| # on the rendered page and in the serialized content, which is where it lives. | ||
| def assert_mention_in_editor(person) | ||
| within find_editor.selector do | ||
| assert_selector %(action-text-attachment[content-type="application/vnd.actiontext.mention"]), | ||
| text: person.name, visible: :all | ||
| end | ||
| end | ||
|
|
||
| # The value the form submits: attachment content re-serialized after sanitizing. | ||
| def assert_comment_in_saved_value | ||
| attachment = Capybara.string(find_editor.value) | ||
| .find(%(action-text-attachment[content-type="application/vnd.actiontext.mention"])) | ||
|
|
||
| content = CGI.unescapeHTML(attachment["content"]) | ||
|
|
||
| assert_includes content, COMMENT, | ||
| "the comment inside attachment content was dropped, which is what SAFE_FOR_XML does without the preservation hook" | ||
| end | ||
| end | ||
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.
Uh oh!
There was an error while loading. Please reload this page.