Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions includes/acf-meta-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ function acf_get_option_meta( $prefix = '' ) {
global $wpdb;

// Vars.
$meta = array();
$search = "{$prefix}_%";
$_search = "_{$prefix}_%";

// Escape underscores for LIKE.
$search = str_replace( '_', '\_', $search );
$_search = str_replace( '_', '\_', $_search );
$meta = array();

// Escape the literal prefix for use in a LIKE clause, then append the
// trailing wildcard. esc_like() escapes %, _ and \ — a manual underscore
// replacement leaves % and \ active, letting a crafted prefix widen the
// match to unrelated option rows.
$search = $wpdb->esc_like( "{$prefix}_" ) . '%';
$_search = $wpdb->esc_like( "_{$prefix}_" ) . '%';

// Query database for results.
$rows = $wpdb->get_results(
Expand Down
134 changes: 134 additions & 0 deletions tests/php/includes/functions/test-acf-option-meta-like-escaping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php
/**
* Tests that acf_get_option_meta() escapes its LIKE patterns with esc_like().
*
* The function builds wp_options LIKE patterns from a caller supplied
* prefix. The previous str_replace() approach escaped only `_`, leaving
* other LIKE metacharacters in the prefix unescaped; these tests assert
* the prefix is run through $wpdb->esc_like(), which covers `%`, `_` and
* `\`, so the prefix matches only as a literal.
*
* The query itself is a no-op under WorDBless, so the generated SQL is
* captured via the wordbless_wpdb_query_results filter and the LIKE
* fragments are compared against patterns built with the same
* $wpdb->prepare()/esc_like() primitives the function uses (avoiding any
* dependency on prepare()'s internal escaping representation).
*
* @package wordpress/secure-custom-fields
*/

use WorDBless\BaseTestCase;

/**
* Tests that acf_get_option_meta() escapes LIKE metacharacters in its prefix.
*/
class Test_ACF_Option_Meta_Like_Escaping extends BaseTestCase {

/**
* The most recent SQL string passed to $wpdb->query().
*
* @var string
*/
protected $captured_sql = '';

/**
* Capture every query the dbless wpdb would run.
*/
public function set_up() {
parent::set_up();
$this->captured_sql = '';
add_filter( 'wordbless_wpdb_query_results', array( $this, 'capture_query' ), 10, 2 );
}

/**
* Remove the capture filter.
*/
public function tear_down() {
remove_filter( 'wordbless_wpdb_query_results', array( $this, 'capture_query' ), 10 );
parent::tear_down();
}

/**
* Filter callback: record the SQL and return an empty result set.
*
* @param array $results The (empty) results array.
* @param string $query The SQL passed to wpdb::query().
* @return array
*/
public function capture_query( $results, $query ) {
// Only retain the acf_get_option_meta lookup, not unrelated queries.
if ( false !== strpos( (string) $query, 'option_name LIKE' ) ) {
$this->captured_sql = (string) $query;
}
return $results;
}

/**
* The escaped fragment a correctly-escaped prefix produces in the SQL.
*
* @param string $literal The literal portion of the LIKE pattern.
* @return string
*/
protected function expected_like_fragment( $literal ) {
global $wpdb;
// Mirror acf_get_option_meta(): esc_like() the literal, append the
// single intended trailing wildcard, then quote it as prepare() does.
return $wpdb->prepare( '%s', $wpdb->esc_like( $literal ) . '%' );
}

/**
* A prefix containing the % wildcard must be escaped, not left active.
*/
public function test_percent_prefix_is_escaped() {
global $wpdb;

acf_get_option_meta( '%' );

$this->assertNotEmpty( $this->captured_sql, 'The option-meta query should have been captured.' );

// The fixed function escapes the whole literal "%_" then appends "%".
$good = $this->expected_like_fragment( '%_' );
$this->assertStringContainsString(
$good,
$this->captured_sql,
'The prefix should be escaped via esc_like().'
);

// The previous behaviour escaped only "_", leaving the leading %
// active. That exact fragment must no longer appear.
$bad = $wpdb->prepare( '%s', str_replace( '_', '\_', '%_%' ) );
$this->assertStringNotContainsString(
$bad,
$this->captured_sql,
'The unescaped (leading-wildcard) pattern must not be generated.'
);
}

/**
* A prefix containing an _ wildcard must be escaped as a literal.
*/
public function test_underscore_prefix_is_escaped() {
acf_get_option_meta( 'a_b' );

$good = $this->expected_like_fragment( 'a_b_' );
$this->assertStringContainsString(
$good,
$this->captured_sql,
'Underscores in the prefix should be escaped as literals.'
);
}

/**
* A benign prefix still produces the expected (unchanged) pattern.
*/
public function test_benign_prefix_unchanged() {
acf_get_option_meta( 'options' );

$good = $this->expected_like_fragment( 'options_' );
$this->assertStringContainsString(
$good,
$this->captured_sql,
'A benign prefix should match option names beginning with it.'
);
}
}
Loading