Skip to content

v1.9.1#763

Open
ilicfilip wants to merge 5 commits into
mainfrom
filip/sanitize-recommendations-title
Open

v1.9.1#763
ilicfilip wants to merge 5 commits into
mainfrom
filip/sanitize-recommendations-title

Conversation

@ilicfilip
Copy link
Copy Markdown
Collaborator

No description provided.

ilicfilip added 2 commits May 26, 2026 11:36
An authenticated Editor (or higher) could create a recommendation via
POST /wp/v2/prpl_recommendations with an HTML payload in the `title`
field (e.g. `<img src=x onerror=alert(1)>`). The dashboard JS template
(views/js-templates/suggested-task.html) renders `title.rendered` with
Underscore's unescaped `{{{ }}}` syntax, so the payload executed when an
admin loaded the dashboard.

Defense in depth:

- Input: add a `rest_pre_insert_prpl_recommendations` filter that strips
  tags from `post_title` on every REST insert/update, regardless of the
  user's `unfiltered_html` capability. Recommendation titles are plain
  text, so this neutralizes the payload at the source.
- Output (JS): route the two raw `{{{ }}}` title sinks through a new
  `prplSuggestedTask.sanitizeTitle()` helper, which inert-parses the
  value with DOMParser (no script/resource side effects) and re-escapes
  it, preserving legitimate entities like `&amp;` without double-encoding
  the server-side `esc_html`'d provider titles.
- Output (admin bar): the PRPL debug tool printed `post_title` unescaped
  into a `WP_Admin_Bar` node id (an HTML attribute) and title (rendered
  as raw HTML), firing the payload on every admin page in debug mode.
  Escape the title with `esc_html()`, use the post ID for the node id,
  and escape the activities node title too.
- Also switch `updateTaskTitle` to set `.textContent` instead of
  `.innerHTML` for the screen-reader label, closing a self-XSS sink.

Adds tests/phpunit/test-class-rest-recommendations-xss.php covering
Editor and Administrator payloads plus a plain-text regression check.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Test on Playground
Test this pull request on the Playground
or download the zip

@tacoverdo
Copy link
Copy Markdown
Contributor

tacoverdo commented May 26, 2026

When trying to reproduce the bug and fix, I ran into the following:

Update_191::sanitize_recommendation_titles() calls wp_update_post() with the stripped title. When the original was pure markup (e.g., ), wp_strip_all_tags() returns ''. If the post also has
empty content + excerpt, WordPress rejects the update with empty_content. update_recommendation() returns false, the migration silently moves on, and the malicious row stays in the DB — still exploitable
when rendered.

Repro on the PR branch:

wp post create --post_type=prpl_recommendations --post_title='<img src=x onerror=alert(1)>' --user=1
  wp post term set <ID> prpl_recommendations_provider <non-user-term> --by=id
  wp option delete progress_planner_version
  # trigger migration (e.g., load any admin page)
  wp eval 'echo get_post(<ID>)->post_title;'   # still raw markup

Suggested fix (in order of preference):

  1. Trash the row when sanitized title is empty — the recommendation was junk anyway:>
if ( '' === $sanitized ) {
     wp_delete_post( $recommendation->ID, true );
     continue;
 }
  1. Direct $wpdb update to bypass WP's empty-content guard:
$wpdb->update( $wpdb->posts, [ 'post_title' => $sanitized ], [ 'ID' => $recommendation->ID ] );
clean_post_cache( $recommendation->ID );
  1. Placeholder fallback — preserves the row but with a safe stub.

Also worth flagging: rest_pre_insert_prpl_recommendations only covers REST writes. wp_insert_post/CLI/programmatic creates bypass it. Moving the sanitizer to wp_insert_post_data (or
save_post_prpl_recommendations) would close that gap too — which is the same WP-CLI bypass we used to plant post 262 in the first place.

A title that is pure markup strips to an empty string. wp_update_post()
rejects an update that would leave the title, content, and excerpt all
empty, so the malicious title was left in the DB. The plugin never stores
title-less recommendations, so delete such rows instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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