Skip to content

Fix: sanitize field_id input and escape rel attribute in oEmbed handler - #1559

Merged
jtsternberg merged 7 commits into
CMB2:developfrom
thisismyurl:fix/oembed-field-id-sanitize-escape
Jun 4, 2026
Merged

jtsternberg merged 7 commits into
CMB2:developfrom
thisismyurl:fix/oembed-field-id-sanitize-escape

Conversation

@thisismyurl

Copy link
Copy Markdown
Contributor

Hey folks,

Two related issues in CMB2_Ajax::oembed_handler() and get_oembed():

  1. $_REQUEST['field_id'] is accessed without an isset() guard (PHP notice if absent) and without sanitize_text_field()/wp_unslash() before passing it into the embed args array.

  2. In get_oembed(), that value is concatenated directly into an HTML rel attribute — rel="' . $oembed['args']['field_id'] . '" — without esc_attr(). WordPress Plugin Check flags this, and it's a reflected XSS vector for any admin with access to an oEmbed field.

Two-line fix: isset() + sanitize_text_field( wp_unslash() ) at input, esc_attr() at the rel attribute on output.

(full disclosure, I used AI help with the testing and tweaks - Christopher)

`$_REQUEST['field_id']` was passed to `get_oembed()` directly —
no `isset()` guard (PHP notice if absent) and no sanitization.
In `get_oembed()`, the value was concatenated into an HTML attribute
(`rel="..."`) without `esc_attr()`, which is a WordPress Plugin Check
violation and a potential reflected XSS vector in admin.

Two-line fix:
1. Sanitize at collection: `isset()` guard + `sanitize_text_field( wp_unslash() )`
2. Escape at output: `esc_attr()` on the `rel` attribute value

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Copilot AI review requested due to automatic review settings June 1, 2026 18:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Security hardening for the CMB2 oEmbed AJAX handler by sanitizing input and escaping output for the field_id request parameter.

Changes:

  • Sanitize $_REQUEST['field_id'] with sanitize_text_field( wp_unslash( ... ) ) and default to an empty string when not set.
  • Escape field_id with esc_attr() when rendered into the rel attribute of the remove-embed link.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request improves security and robustness in includes/CMB2_Ajax.php by sanitizing and escaping the field_id parameter. The review feedback correctly points out that $_REQUEST['field_id'] should be validated as a string before sanitization to prevent potential PHP warnings or TypeErrors on PHP 8.x if an array is provided.

Comment thread includes/CMB2_Ajax.php
'object_type' => isset( $_REQUEST['object_type'] ) ? $_REQUEST['object_type'] : 'post',
'oembed_args' => $embed_args,
'field_id' => $_REQUEST['field_id'],
'field_id' => isset( $_REQUEST['field_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['field_id'] ) ) : '',

@gemini-code-assist gemini-code-assist Bot Jun 1, 2026 •

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.

medium

If $_REQUEST['field_id'] is passed as an array (e.g., ?field_id[]=value), passing it directly to wp_unslash() and sanitize_text_field() will trigger a PHP warning or a TypeError on PHP 8.x because sanitize_text_field() expects a string.

To ensure robustness and prevent potential crashes on PHP 8.x, verify that the input is a string using is_string() before sanitizing.

			'field_id'    => ! empty( $_REQUEST['field_id'] ) && is_string( $_REQUEST['field_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['field_id'] ) ) : '',

@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

thisismyurl and others added 6 commits June 2, 2026 11:59
Address review feedback: if field_id is passed as an array
(?field_id[]=value), sanitize_text_field( wp_unslash( array ) )
triggers a PHP 8 TypeError. Added is_string() guard so array input
falls back to the empty-string default, matching the intent of the
original isset() guard.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Locks in the esc_attr() output escaping added in this PR. Verified RED by
temporarily reverting the esc_attr() call — the raw, attribute-breaking
field_id ('evil" onmouseover="alert(1)') was reflected verbatim into rel="";
GREEN with the escaping in place. Runs in the default suite via get_oembed()
(not the excluded ajax group).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
object_id was read unguarded one line above the field_id fix, emitting an
"Undefined array key" warning (PHP 8+) when absent — the same class of notice
the field_id guard addresses. Mirror that pattern: isset() guard +
sanitize_text_field( wp_unslash() ).

TDD: added test_oembed_handler_tolerates_missing_object_id, which drives
oembed_handler() with object_id absent (capturing wp_send_json's wp_die via a
filtered handler). Verified RED first — the test errored with
'Undefined array key "object_id"' at CMB2_Ajax.php:94 — then GREEN with the
guard in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Aligns the oembed_url input handling with the field_id/object_id pattern
(wp_unslash before sanitize_text_field). Behavior-preserving: the value is
immediately esc_url()'d, and esc_url() already strips backslashes, so unslashing
first produces identical output — hence no dedicated test; the existing
Test_CMB2_Ajax suite stays green (7 tests, 14 assertions).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…th a test

7e91829 added `&& is_string( $_REQUEST['field_id'] )` to guard against a claimed
PHP 8 TypeError from sanitize_text_field( wp_unslash( array ) ). That premise is
incorrect: WordPress's _sanitize_text_fields() returns '' for array/object input
(wp-includes/formatting.php), and wp_unslash() (stripslashes_deep → map_deep) is
array-safe. So the existing isset() guard already collapses an array field_id to
the empty-string default with no error, on every supported WP (the array/object
early-return has been in core since WP 4.7; CMB2 supports WP 6.3+).

Restore the clean isset() + sanitize_text_field( wp_unslash() ) pattern, keeping
field_id consistent with the object_id line.

Adds test_oembed_handler_handles_array_field_id, which drives oembed_handler()
with field_id = array(...) and asserts it returns success with an empty rel=""
(no PHP error). Verified green both with the guard present and after removing it,
proving the removal is behavior-preserving and pinning the behavior so the guard
is not reintroduced on the same false premise.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
get_oembed()/oembed_handler() mutate the shared cmb2_ajax() singleton
(ajax_update, hijack, object_id/type) and register hijack_oembed_cache_*
metadata filters on it. Because it's a singleton, that state persisted across
tests: the new oembed_handler() tests left ajax_update=true, which flipped the
later network-dependent Test_CMB2_Types_Display::test_oembed onto its fallback
branch (where a stale "no_connection" verifier then failed).

Reset the singleton's mutable state and remove its hijack filters in tear_down,
restoring constructor defaults between tests. Full suite green and stable across
3 consecutive runs (217 tests, 708 assertions).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jtsternberg

Copy link
Copy Markdown
Member

Pushed a few follow-up commits to the branch (TDD throughout — each behavior has a test that was watched fail before the fix):

Reverted the is_string() guard (7e91829). Thanks for chasing the bot feedback, but that guard addresses an error that can't actually occur. I checked against WP core:

  • sanitize_text_field() → _sanitize_text_fields() returns '' immediately for any array/object (wp-includes/formatting.php: if ( is_object( $str ) || is_array( $str ) ) { return ''; }).
  • wp_unslash() → stripslashes_deep() → map_deep() is array-safe — an array passes through untouched.

So sanitize_text_field( wp_unslash( array ) ) yields '' with no TypeError on every supported WP (that early-return has been in core since WP 4.7; CMB2 supports 6.3+). The original isset() guard already collapses ?field_id[]=x to the empty-string default. I restored the clean isset() + sanitize_text_field( wp_unslash() ) pattern and added test_oembed_handler_handles_array_field_id, which drives the handler with an array field_id and asserts an empty rel="" with no error — green both with and without the guard, so it pins the behavior and documents why the guard isn't needed.

Other commits:

  • test: assert field_id is escaped in the rel attribute (locks the esc_attr() fix; verified RED by reverting the escape).
  • fix: guard + sanitize $_REQUEST['object_id'] — it was read unguarded one line above the field_id fix, emitting the same "Undefined array key" warning when absent. TDD'd via a handler test.
  • refactor: wp_unslash() oembed_url before sanitize, for consistency (behavior-preserving — the value is esc_url()'d immediately, and esc_url() already strips backslashes).
  • test: reset the CMB2_Ajax singleton in tear_down() so the new handler tests don't leak state into later tests.

Full PHPUnit suite green and stable (217 tests, 708 assertions).

@jtsternberg
jtsternberg merged commit 9310aa1 into CMB2:develop Jun 4, 2026
18 checks passed
jtsternberg added a commit that referenced this pull request Jun 4, 2026
jtsternberg added a commit that referenced this pull request Jun 18, 2026
Lead with user-facing summary + why (WPCS escaping flag) instead of
listing internal Class::method() signatures, matching the #1559 sibling.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.

3 participants