Skip to content

Instantly share code, notes, and snippets.

@mikeschinkel
Last active July 27, 2021 12:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeschinkel/b2a611d37be540b1a738325b06a6af86 to your computer and use it in GitHub Desktop.
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
<?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);
<?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