v1.9.1#763
Conversation
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 `&` 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.
|
Test on Playground |
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>
Migration fix verified — but write-side bypass is still openWhat works nowMigration empty-content bug is fixed. Existing title-only payloads (e.g. What's still broken: WP-CLI /
|
|
@tacoverdo , we discussed this yesterday - are we good? |
* Fix PHPStan errors and phpunit CVE on main Brings main's static analysis and dependency security checks back to green: - Static Analysis: clear 25 pre-existing PHPStan errors. Ports develop's typed @return on Date::get_periods()/get_range() (which also resolves the Chart modify() errors), takes develop's exact versions of class-page-settings, class-activity-scores, class-chart and class-update-140, converts the WP-core require_once ignores to the @phpstan-ignore-next-line form that suppresses under PHPStan 2.1.x, and adds inline ignores elsewhere. - Security check: bump phpunit/phpunit 9.6.30 -> 9.6.34 in composer.lock to resolve CVE-2026-24765 (unsafe deserialization in PHPT code coverage). * Fix abstract method fatal in test-class-security.php The anonymous classes extending the abstract Tasks_Interactive did not implement the abstract Tasks::should_add_task() method. phpunit 9.6.30 did not surface this, but 9.6.34 (the CVE-2026-24765 fix) does, causing a fatal when the test class loads. Implement should_add_task() in all 8 anonymous task providers. * v1.9.1 (#763) * Sanitize and escape prpl_recommendations title 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 `&` 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. * Bump version to 1.9.1 * add migration script and revert JS title escaping * add inline comment, cc @tacoverdo * Delete recommendation when sanitized title is empty 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> * update readme.txt --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix plain-text title test to pass on multisite On multisite, editors lack the unfiltered_html capability, so core's kses encodes the ampersand in the test title and the byte-for-byte assertion fails. Grant the capability (via super admin on multisite) so the test isolates our XSS sanitization rather than core's kses behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Grant unfiltered_html before switching user in title test kses_init() runs on the set_current_user hook and decides whether to attach the kses filters at switch time. The capability must be granted before wp_set_current_user(), otherwise the filters are already attached and the multisite assertion still sees the ampersand encoded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Bump composer/composer 2.9.2 -> 2.10.0 to clear dev-dependency CVEs Resolves the Security check failure: composer/composer 2.9.2 (pulled in transitively via wp-cli/wp-cli-bundle in require-dev) carried CVE-2026-40176, CVE-2026-40261, and CVE-2026-45793. Targeted `composer update composer/composer --with-dependencies`; composer.json (runtime deps) unchanged. `composer audit` now reports no advisories. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Joost de Valk <joost@altha.nl> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No description provided.