Make WordPress Core

Opened 5 weeks ago

Last modified 13 days ago

#65051 assigned defect (bug)

$_REQUEST['term'] used unsanitized in user search query

Reported by: rajeshcp's profile rajeshcp Owned by: rajeshcp's profile rajeshcp
Milestone: 7.1 Priority: normal
Severity: normal Version:
Component: Networks and Sites Keywords: has-patch needs-testing has-test-info has-unit-tests
Focuses: multisite, coding-standards Cc:

Description

User-supplied search term is concatenated directly into the get_users() search argument without

sanitize_text_field() or wp_unslash().

Attachments (1)

test-65051-sanitize.png (48.2 KB) - added by liaison 4 weeks ago.
test-65051-sanitize

Download all attachments as: .zip

Change History (8)

This ticket was mentioned in PR #11530 on WordPress/wordpress-develop by rajeshcpr.


5 weeks ago
#1

User-supplied search term is concatenated directly into the get_users() search argument without

sanitize_text_field() or wp_unslash().

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

Fixes #65051

## Use of AI Tools

#2 @gaurangsondagar
5 weeks ago

  • Focuses coding-standards added

Tested the patch and confirmed the issue: https://github.com/WordPress/wordpress-develop/pull/11530/commits/5eed1c8ea50eb3dfda7605749f267bf9e3234dc3

Environment:

  • WordPress: 7.1-alpha-62161-src
  • PHP: 8.3.30
  • Browser: Chrome
  • Database: MySQL 8.4.8
  • OS: Ubuntu

1) The current implementation uses $_REQUEST[term] directly without sanitization.
2) The patch correctly applies 'wp_unslash()' and 'sanitize_text_field()', which aligns with WordPress data handling standards.
3) Verified that the user search functionality continues to work as expected after the change.

This is a valid security improvement and works as expected

Screenshot for reference: https://kommodo.ai/i/s2Bol19v4cwB50UNttQp

#3 @audrasjb
5 weeks ago

  • Component changed from General to Networks and Sites
  • Focuses multisite added
  • Milestone changed from Awaiting Review to 7.1
  • Severity changed from major to normal
  • Version trunk deleted

#4 @liaison
4 weeks ago

Test Report & Security Impact Analysis

  1. Environment

WordPress: 7.1-alpha (trunk)

Setup: Multisite Network Admin

Verification Method: Standalone PHP Mock + Browser DevTools Interception

  1. Security Analysis: Why a PoC is currently "Silent"

I would like to clarify the current impact of this vulnerability. While the fix is essential, a successful XSS alert is currently unlikely to trigger in the default UI for the following reasons:

Empty Response on Malicious Payload: When a payload like <script>alert('XSS')</script> is injected into the term parameter, the backend executes a get_users query. Since no user account exists with such a string, the database returns an empty set. The resulting Ajax response is an empty array [].

Safe Client-Side Handling: The current core JavaScript expects a JSON array of user objects. When it receives [], it simply does not render any dropdown items, preventing the payload from being echoed back into the DOM.

  1. Why the fix is still critical

Log Poisoning / Second-Order XSS: If the search term is recorded in server-side logs (e.g., Audit logs or Slow Query logs) viewed in other admin interfaces, the script could execute.

Future-Proofing: Any UI changes displaying "No results found for: [term]" would immediately turn this into a high-risk Reflected XSS.

Data Integrity: Aligning with WP coding standards by sanitizing at the entry point.

  1. Verification Results

[Next comment]My standalone tests confirm that the patch correctly strips HTML tags at the entry point.

Payload Intercepted via DevTools:

action: autocomplete-user
term: <script>alert('XSS')</script>Admin
site_id: 2
Backend Observation (using attached test script):

Before Patch: get_users receives the raw <script> tag.

After Patch: get_users receives the sanitized string alert('XSS')Admin.

test-65051-sanitize

Last edited 4 weeks ago by liaison (previous) (diff)

@liaison
4 weeks ago

test-65051-sanitize

#5 @liaison
4 weeks ago

Test Report
Ticket: #65051 - $_REQUEST[\'term\'] used unsanitized in user search query

Environment
WordPress Version: 7.1-alpha (trunk)

PHP Version: 8.x

Test Method: Standalone Mock / Integration Test

OS: Windows (MINGW64)

Testing Methodology
I performed a deep-dive verification using a standalone mock script to isolate the data flow within wp_ajax_autocomplete_user(). By stubbing the core dependencies (is_multisite, get_users, etc.), I was able to intercept the exact arguments being passed to the user query logic.

Test Results

  1. Confirming the Vulnerability (Before Patch)

Using a malicious payload: <script>alert('hack')</script>Admin
The input was passed directly to the search argument without any sanitization.

Intercepted Query: [Intercepted] get_users() called with 'search' => '<script>alert(\'hack\')</script>Admin'

Status: ❌ Confirmed. Raw HTML/Script tags reached the query level.

  1. Verification of Fix (After Patch)

Applied sanitize_text_field( wp_unslash( ... ) ) to the $term variable.

Intercepted Query: [Intercepted] get_users() called with 'search' => '*alert(\'hack\')Admin*'

Status: ✅ Fixed. The <script> tags were successfully stripped before reaching get_users().

Execution Log Output (before patch)

--- Running Standalone Invocation Test ---

--- [Demo before patch] ---
[Intercepted] get_users() called with 'search' => '<script>alert(\'hack\')</scri p
t>Admin'
[JSON Response]: {"success":true}

--- [Demo after patch] ---
[Intercepted] get_users() called with 'search' => 'alert(\'hack\')Admin'
[JSON Response]: {"success":true}

--- [Test Target: wp_ajax_autocomplete_user] ---
[Intercepted] get_users() called with 'search' => '*<script>alert(\'hack\')</sc ipt>Admin*'

[wp_die] Value: []

Execution Log Output (after patch)

Plaintext
--- Running Standalone Invocation Test ---

--- [Demo before patch] ---
[Intercepted] get_users() called with 'search' => '<script>alert(\'hack\')</script>Admin'
[JSON Response]: {"success":true}

--- [Demo after patch] ---
[Intercepted] get_users() called with 'search' => 'alert(\'hack\')Admin'
[JSON Response]: {"success":true}

--- [Test Target: wp_ajax_autocomplete_user] ---
[Intercepted] get_users() called with 'search' => '*alert(\'hack\')Admin*'
[wp_die] Value: []

Verdict
The patch effectively resolves the issue by sanitizing the user-supplied search term. It prevents potential XSS payloads from being processed in the backend logic while maintaining the expected search functionality.

test-65051-sanitize.php

<?php
/**
 * Standalone Test: Invoking wp_ajax_autocomplete_user directly
 */

define( 'DOING_AJAX', true );
define( 'WP_ADMIN', true );

// fake $wp_db 
$GLOBALS['wpdb'] = unserialize('O:8:"stdClass":0:{}'); 

// --- Stub Functions (to pass test)---
function auth_redirect() {} 
function check_ajax_referer( $action ) { return true; } 
function current_user_can( $cap ) { return true; } 

function wp_unslash( $data ) { return stripslashes( $data ); }
function sanitize_text_field( $str ) { return strip_tags( $str ); }

function get_users( $args ) {
    $search_term = $args['search'] ?? '(no search term)';

    if ( '(no search term)' !== $search_term ) {
        echo "[Intercepted] get_users() called with 'search' => " . var_export($search_term, true) . "\n";
    }
    
    return array(); 
}

function wp_send_json( $response ) {
    echo "[JSON Response]: " . json_encode( $response ) . "\n";
}

function wp_die( $msg = '' ) { 
    echo "\n[wp_die] Value: $msg\n";
}

if ( ! function_exists( 'is_multisite' ) ) {
    function is_multisite() {
        return true; 
    }
}

if ( ! function_exists( 'wp_is_large_network' ) ) {
    function wp_is_large_network() {
        return false; 
    }
}

if ( ! function_exists( 'get_current_blog_id' ) ) {
   function get_current_blog_id() {
        return 1;
    }
}


if ( ! function_exists( 'wp_json_encode' ) ) {
   function wp_json_encode( $data ) {
        return json_encode( $data );
    }
}

if ( ! function_exists( 'get_current_screen' ) ) {
    function get_current_screen() { return null; }
}

if ( ! function_exists( 'wp_parse_args' ) ) {
    function wp_parse_args( $args, $defaults = array() ) {
        return array_merge( $defaults, $args );
    }
}

// --- target source ---
require_once 'wp-admin/includes/ajax-actions.php'; 

/**
 * Simulation Demo
 */

function wp_ajax_autocomplete_user_demo_before_patch() {
    $term = $_REQUEST['term']; 
 
    get_users( array(
        'search' => $term,
        'fields' => array( 'ID', 'user_login' ),
    ) );

    wp_send_json( array( 'success' => true ) );
}

function wp_ajax_autocomplete_user_demo_after_patch() {
    $term = sanitize_text_field( wp_unslash( $_REQUEST['term'] ) );

    get_users( array(
        'search' => $term,
        'fields' => array( 'ID', 'user_login' ),
    ) );

    wp_send_json( array( 'success' => true ) );
}


echo "--- Running Standalone Invocation Test ---\n";

$_REQUEST['term'] = "<script>alert('hack')</script>Admin";

echo "\n--- [Demo before patch] ---\n";
try {
    wp_ajax_autocomplete_user_demo_before_patch();
} catch (Exception $e) {
    echo "Caught: " . $e->getMessage();
}

echo "\n--- [Demo after patch] ---\n";
try {
    wp_ajax_autocomplete_user_demo_after_patch();
} catch (Exception $e) {
    echo "Caught: " . $e->getMessage();
}

echo "\n--- [Test Target: wp_ajax_autocomplete_user] ---\n";
try {
    wp_ajax_autocomplete_user();
} catch (Exception $e) {
    echo "Caught: " . $e->getMessage();
}

#6 @liaison
4 weeks ago

  • Keywords has-test-info added

#7 @liaison
13 days ago

  • Keywords has-unit-tests added
Note: See TracTickets for help on using tickets.