Last active
July 27, 2021 12:52
-
-
Save mikeschinkel/b2a611d37be540b1a738325b06a6af86 to your computer and use it in GitHub Desktop.
How we might be able to move to using parameterized queries in WordPress core
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Put this in wp-config.php | |
define('PREPARED_SQL_REQUIRED',true); | |
// A site builder who wants to use prepared statements and parameterized queries | |
// could run these in various hooks before $wpdb->query() below is run. | |
$question_fragment = $wpdb->prepare( '`question_id` = %d', $question_id ); | |
$answer_fragment = $wpdb->prepare( '`answer_name` = %s', $new_answer ); | |
$wpdb->compose('UPDATE %s polls SET vote = vote+1 WHERE %s AND %s', | |
$question_fragment, | |
$answer_fragment); | |
// The errant plugin could then run this code with an injection vulnerability | |
// but our query will bypass the exploit and use parameterized values instead. | |
// @see https://www.exploit-db.com/exploits/50052 | |
$wpdb->query('UPDATE '.$wpdb->prefix.'polls SET vote = vote+1 | |
WHERE `question_id` = '.$question_id.' AND `answer_name` = '.$new_answer); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
function parse_placeholders( $query ): string { | |
return; // Assume this function parses out %s, %d, etc. | |
} | |
class WP_Error { | |
function __construct(string $msg) {} | |
} | |
class wpdb { | |
private static $_queries = array(); | |
/** | |
* @var mysqli $conn | |
*/ | |
public $conn; | |
function prepared_required():bool{ | |
return defined( 'PREPARED_SQL_REQUIRED' )&& PREPARED_SQL_REQUIRED; | |
} | |
function compose( $template, ...$fragments ):string|WP_Error { | |
begin: | |
{ | |
$query = null; | |
if ( ! $this->prepared_required() ) { | |
goto end; | |
} | |
foreach( $fragments as $index => $fragment ) { | |
// Ensure that each of the fragments have been prepared | |
if ( isset( self::$_queries[ $fragment ] ) ) { | |
continue; | |
} | |
$msg = sprintf('ERROR: SQL fragment #%d not prepared',$index); | |
$query = new WP_Error($msg); | |
goto end; | |
} | |
} | |
end: | |
if ( is_null( $query ) ) { | |
$query = $this->prepare( $template, ...$fragments ); | |
} | |
return $query; | |
} | |
function prepare( $query, ...$args ):string { | |
// Assume for this example that this does everything | |
// that is needed to ensure the query is correct. | |
$sql = sprintf( $query, ...$args ); | |
self::$_queries[ $sql ] = array( | |
$query, | |
$args, | |
parse_placeholders( $query ), | |
); | |
return $sql; | |
} | |
function query( $query ) { | |
{ | |
$method = 'unsafe'; | |
if ( ! $this->prepared_required() ) { | |
goto run_query; | |
} | |
if ( isset( self::$_queries[ $query ] ) ) { | |
$method = 'safe'; | |
goto run_query; | |
} | |
trigger_error( 'Potential exploit occurred' ); | |
$method = null; | |
} | |
run_query: | |
switch ( $method ) { | |
case 'unsafe': | |
mysqli_query( $this->conn, $query ); | |
break; | |
case 'safe': | |
$parts = self::$_queries[ $query ]; | |
$stmt = mysqli_prepare( $this->conn, $parts[ 0 ] ); | |
foreach ( $parts[ 1 ] as $index => $param ) { | |
$stmt->bind_param( $parts[ 2 ][ $index ], $param ); | |
} | |
$stmt->execute(); | |
break; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment