Skip to content

KSES: Sanitize srcset by element and attribute in both parsers - #14812

Open
adamsilverstein wants to merge 2 commits into
WordPress:trunkfrom
adamsilverstein:ticket/29807-kses-wiring
Open

adamsilverstein wants to merge 2 commits into
WordPress:trunkfrom
adamsilverstein:ticket/29807-kses-wiring

Conversation

@adamsilverstein

Copy link
Copy Markdown
Member

Trac ticket: https://core.trac.wordpress.org/ticket/29807

Second PR in the stack splitting up #6184. Builds on #14811, so the diff here includes that commit until it lands; the change in this PR is the second commit only.

Both KSES parsers currently pick the URL sanitizer from the attribute name alone, so there is no way to say that srcset on img/source (and imagesrcset on link) is a list of image candidates rather than a single URL. This PR adds a small private dispatcher, _wp_kses_sanitize_url_attribute( $element, $attribute, $value, $allowed_protocols, $uri_attributes ), that takes the element name as well and routes those attributes to wp_kses_sanitize_srcset(); anything in wp_kses_uri_attributes() still goes through wp_kses_bad_protocol() exactly as before, and everything else is untouched. Per @dmsnell's note on #6184 this is hard-coded with no new filter for now.

It is wired into three places:

  • The HTML API sanitizer in wp_sanitize_html_kses(), where the value is already decoded and re-encoded by the processor.
  • wp_kses_hair(), which gains an optional third $element parameter that wp_kses_attr() now passes. Existing callers without it (embed.php, formatting.php) keep the attribute-name-only behavior, with srcset still sanitized as srcset.
  • wp_kses_one_attr(), which now decodes the value before sanitizing and re-encodes it only when sanitization changed it, so   / , separators cannot hide a candidate.

Nothing user-facing changes yet: srcset is not in the allowed post HTML, so it is still stripped. That lands in the third PR. The tests here pass their own $allowed_html and run every case through both parsers, toggling wp_kses_force_legacy_parser.

One thing worth a look: srcset on an element browsers do not read it on (the span case in the tests) is intentionally left alone rather than parsed. That follows the "dispatch by tag" feedback, but if folks would rather sanitize it anyway as belt and braces, that is a one-line change in the dispatcher.

Testing

npm run test:php -- --group kses. New cases are in tests/phpunit/tests/kses.php (test_srcset_is_sanitized_by_element, test_one_attr_sanitizes_srcset) and tests/phpunit/tests/kses/wpKsesHair.php. Same caveat as the first PR: I could not run PHPUnit locally today, so CI is the first full run.

AI Use

Code and description both came out of a conversation with 🤖 Claude Code, working from the review thread on #6184. I will review and test.

A srcset value is a comma-separated list of image candidate strings,
not a single URL, so passing it through wp_kses_bad_protocol() or
esc_url() either misses candidates or corrupts the list. This function
segments the value following the HTML specification's srcset parsing
algorithm and removes candidates whose URL uses a disallowed protocol.

It takes decoded attribute text and returns decoded text, leaving HTML
decoding and encoding to the caller, and returns the input string
itself when every candidate is allowed. Nothing calls it yet; wiring
it into KSES by tag and attribute follows separately.

See https://core.trac.wordpress.org/ticket/29807
Both KSES parsers decide how to sanitize a URL attribute from the
attribute name alone, which cannot express that srcset on img and
source, and imagesrcset on link, hold a list of image candidates
rather than one URL. Route URL attributes through a private dispatcher
that takes the element name as well, sending those attributes to
wp_kses_sanitize_srcset() and everything in wp_kses_uri_attributes()
to wp_kses_bad_protocol() as before.

wp_kses_hair() gains an optional $element parameter so wp_kses_attr()
can pass the element through; existing callers without one keep the
attribute-name-only behavior. wp_kses_one_attr() decodes the value
before sanitizing and re-encodes it only when sanitization changed it,
so encoded separators cannot hide a candidate.

No allowed-HTML change yet, so srcset is still stripped from post
content; that follows separately.

See https://core.trac.wordpress.org/ticket/29807
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

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.

1 participant