Skip to content

Keep an image's alternative text and intrinsic size in attachment content - #1230

Draft
jeremy wants to merge 2 commits into
dompurify-3-4-13from
attachment-content-image-attributes
Draft

Keep an image's alternative text and intrinsic size in attachment content#1230
jeremy wants to merge 2 commits into
dompurify-3-4-13from
attachment-content-image-attributes

Conversation

@jeremy

@jeremy jeremy commented Aug 11, 2026

Copy link
Copy Markdown
Member

Consumer action: none. This widens what an allowed element may carry; it
never admits an element.

An <img> inside a custom attachment is sanitized by Lexxy every time the
attachment renders in the editor, so what it may carry is Lexxy's decision rather
than the host app's. alt and width/height were all being dropped. alt is
what a screen reader reads; width/height are what stop the line reflowing
while the image loads. None of the three can execute anything.

alt goes in the blanket list, since it is inert on every element it can appear
on. srcset deliberately stays out — it carries URLs, so it belongs to a consumer
that declares it.

width/height are scoped to img instead, via a new per-tag mechanism, because
ALLOWED_ATTR is not per-tag: putting them in the blanket list would also permit
<table width="100000"> and <td height="500"> in attachment content, which is
layout the editor previously stripped. The widening only ever applies to tags the
caller already permits.

Verification

Both sides of the round trip, because neither catches the other:

  • test/actiontext/image_attributes_test.rb reads Lexxy's allowlist out of
    source
    rather than copying it, so widening one side without the other fails
    there.
  • test/system/attachment_content_images_test.rb drives the dummy app end to end
    — editor DOM, submitted value, rendered page, re-edited document. That needed
    the dummy mention partial to carry an avatar, since the consumer shape this
    exists for does; a data: URI keeps it self-contained and off the network.
  • The unit tests are mutation-checked: revert the config change and the alt/size
    and the tag-scoping cases fail.

The tag helper test now asserts the mention's parts rather than the partial's
exact markup, so adding an avatar doesn't make it brittle.


Part of a series re-filing #1227 at reviewable scope, after #1227 was reverted
from main in 8c64aa4. Merge order: #1228#1229this → #instance →
#trusted-types → #1226. Nothing here is released.

Draft: needs human review and a soak period before merging.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Preserves image alternative text and intrinsic dimensions through attachment sanitization and Action Text round trips.

Changes:

  • Allows alt globally and width/height specifically on permitted <img> elements.
  • Adds unit, Action Text, and system coverage.
  • Adds an avatar to the dummy mention attachment fixture.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/config/dom_purify.js Extends sanitized image attributes.
test/javascript/unit/dom_purify_image_attributes.test.js Tests attribute preservation and scoping.
test/actiontext/image_attributes_test.rb Checks server-side attribute preservation.
test/system/attachment_content_images_test.rb Tests the complete persistence round trip.
test/dummy/app/views/people/_person.html.erb Adds the test avatar markup.
test/helpers/lexxy/tag_helper_test.rb Makes mention assertions structure-based.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/actiontext/image_attributes_test.rb
@jeremy

jeremy commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Verification

  • Unit, mutation-checked: revert the config change and the alt/size case and the tag-scoping case both fail. The other three keep passing, which is the point — they are the controls that stop this from being a blanket widening.
  • Rails, both CI legs green, including the new test/actiontext/image_attributes_test.rb (3 runs, 16 assertions) and the new system test.
  • bc3: 73 system tests and 132 rich-text/mention/attachment tests, 0 failures, against a build carrying this change.

Nothing here needed a browser beyond what CI already runs — the widening is decided entirely by buildConfig, and the Action Text test reads the allowlist out of source so the two sides can't drift apart silently.

CI: green (9/9).

@jeremy

jeremy commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Handoff note for the whole campaign, including QA results and what still needs a human decision: #1234

@jeremy
jeremy force-pushed the attachment-content-image-attributes branch from 350ebc7 to e520212 Compare August 11, 2026 16:54
jeremy added 2 commits August 11, 2026 22:37
…tent

An <img> inside a custom attachment is sanitized by Lexxy every time the
attachment renders in the editor, so what it may carry is Lexxy's decision
rather than the app's. alt and width/height were both being dropped: alt is
what a screen reader reads, and width/height are what stop the line reflowing
while the image loads. Neither can execute anything.

alt goes in the blanket list, since it is inert on every element it can appear
on. srcset stays out — it carries URLs, so it belongs to a consumer that
declares it.

width/height are scoped to img instead, through a new per-tag mechanism, because
ALLOWED_ATTR is not per-tag: putting them in the blanket list would also permit
`<table width="100000">` and `<td height="500">` in attachment content, which is
layout the editor previously stripped. The widening only ever applies to tags the
caller already permits, so it never admits an element.

Covered on both sides of the round trip. The Action Text test reads Lexxy's
allowlist out of source rather than copying it, so widening one side without the
other fails there; the system test drives the dummy app end to end — editor DOM,
submitted value, rendered page, re-edited document — which needed the mention
partial to carry an avatar, as the consumer shape this exists for does. A data:
URI keeps that self-contained and off the network.

The tag helper test asserts the mention's parts rather than the partial's exact
markup, so adding the avatar doesn't make it brittle.
Review catch. The agreement check intersected the blanket allowlist with a
hardcoded `%w[ alt ]`, so a newly blanket-allowed attribute sat outside it —
while the header claimed "widening one side without the other fails here". It
would not have. Mutation-checked: adding `longdesc` to ALLOWED_HTML_ATTRIBUTES
now fails, and did not before.

It now takes the whole blanket list minus one documented exemption, and pins
the exemption too, so NOT_PERSISTED cannot quietly become a place to hide real
disagreements.

That exemption is `contenteditable` alone. The old comment said Action Text
drops `contenteditable` and `style` "by design"; it keeps `style`. Both sides
agree on it, so it belongs in the check rather than in an exception list.

On reading the constants out of dom_purify.js, which STYLE.md:319 would
otherwise argue against: kept, with the reasoning written into the file. The
property is that two allowlists in two languages agree, and Ruby has no public
interface onto the JavaScript one — copying the list across converts a drift
this test catches into one it cannot see, since the Ruby copy would keep passing
while Lexxy widened underneath it. The coupling is bounded by the vacuity guard,
which fails loudly on a rename instead of passing on an empty list.
@jeremy
jeremy force-pushed the attachment-content-image-attributes branch from e520212 to 340d4b5 Compare August 12, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants