From 654d905f6bef3294b8ef74742fb429f46c59b18a Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Thu, 11 Jun 2026 10:53:27 -0400 Subject: [PATCH] fix: use prepared identifier placeholders in table strategies Replace string-interpolated table/column identifiers with $wpdb->prepare() and the %i identifier placeholder (WP 6.2+) across QueryStrategy::estimatedCount, the empty-row INSERT in CanQueryWordPressDatabase, TableDeleteStrategy's DROP TABLE, and TableCreate/TableUpdateStrategy's DDL. Also fixes two real misuses in TableUpdateStrategy::getCurrentColumns: TABLE_NAME was interpolated into value position (now a %s placeholder) and prepare() was called with zero arguments, which triggers _doing_it_wrong on every column sync. Identifiers all resolve from internal Table objects today, so this is hardening plus correct wpdb usage rather than a live injection fix. Requires WordPress 6.2+ for %i. Fixes #33 --- lib/Strategies/QueryStrategy.php | 2 +- lib/Strategies/TableCreateStrategy.php | 6 +++++- lib/Strategies/TableDeleteStrategy.php | 2 +- lib/Strategies/TableUpdateStrategy.php | 14 +++++++++----- lib/Traits/CanQueryWordPressDatabase.php | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/Strategies/QueryStrategy.php b/lib/Strategies/QueryStrategy.php index 3aaa6d7..d1d5f95 100644 --- a/lib/Strategies/QueryStrategy.php +++ b/lib/Strategies/QueryStrategy.php @@ -40,7 +40,7 @@ public function update(Table $table, array $where, array $data): void public function estimatedCount(Table $table): int { global $wpdb; - $rows = $wpdb->get_var("SELECT COUNT(*) FROM " . $table->getName()); + $rows = $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM %i', $table->getName())); if ($rows !== null) { return (int)$rows; diff --git a/lib/Strategies/TableCreateStrategy.php b/lib/Strategies/TableCreateStrategy.php index 3d3999d..607b774 100644 --- a/lib/Strategies/TableCreateStrategy.php +++ b/lib/Strategies/TableCreateStrategy.php @@ -39,13 +39,17 @@ public function create(Table $table): void */ protected function buildCreateQuery(Table $table): string { + global $wpdb; + $args = Arr::process([$this->convertColumnsToSqlString($table), $this->convertIndicesToSqlString($table)]) ->whereNotEmpty() ->setSeparator(",\n ") ->toString(); + $tableIdentifier = $wpdb->prepare('%i', $table->getName()); + return <<getName()} ( + CREATE TABLE IF NOT EXISTS {$tableIdentifier} ( $args ) CHARACTER SET {$table->getCharset()} COLLATE {$table->getCollation()}; SQL; diff --git a/lib/Strategies/TableDeleteStrategy.php b/lib/Strategies/TableDeleteStrategy.php index 8d0a49a..20ec174 100644 --- a/lib/Strategies/TableDeleteStrategy.php +++ b/lib/Strategies/TableDeleteStrategy.php @@ -21,7 +21,7 @@ class TableDeleteStrategy implements CoreTableDeleteStrategy public function delete(string $tableName): void { try { - $this->wpdbQuery("DROP TABLE IF EXISTS $tableName"); + $this->wpdbQuery('DROP TABLE IF EXISTS %i', $tableName); } catch (DatastoreErrorException $e) { throw new TableDropFailedException($e->getMessage(), $e->getCode(), $e); } diff --git a/lib/Strategies/TableUpdateStrategy.php b/lib/Strategies/TableUpdateStrategy.php index 778c707..5623c67 100644 --- a/lib/Strategies/TableUpdateStrategy.php +++ b/lib/Strategies/TableUpdateStrategy.php @@ -57,11 +57,11 @@ protected function convertColumnToSql(Column $column): string protected function getCurrentColumns(string $tableName): array { global $wpdb; - $query = "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA + $query = 'SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_NAME = '{$tableName}'"; + WHERE TABLE_NAME = %s'; - $results = $wpdb->get_results($wpdb->prepare($query), ARRAY_A); + $results = $wpdb->get_results($wpdb->prepare($query, $tableName), ARRAY_A); $columns = []; foreach ($results as $row) { @@ -101,6 +101,8 @@ protected function needsColumnModification(array $currentColumnData, Column $new */ protected function buildSyncColumnsQuery(Table $table): ?string { + global $wpdb; + $currentColumns = $this->getCurrentColumns($table->getName()); $newColumns = $table->getColumns(); $queries = []; @@ -124,7 +126,7 @@ protected function buildSyncColumnsQuery(Table $table): ?string // Drop columns that no longer exist in the new definition foreach ($currentColumns as $currentColumnName => $currentColumnData) { if (!in_array($currentColumnName, $newColumnNames)) { - $queries[] = "DROP COLUMN `{$currentColumnName}`"; + $queries[] = 'DROP COLUMN ' . $wpdb->prepare('%i', $currentColumnName); } } @@ -137,8 +139,10 @@ protected function buildSyncColumnsQuery(Table $table): ?string return null; } + $tableIdentifier = $wpdb->prepare('%i', $table->getName()); + return <<getName()} + ALTER TABLE {$tableIdentifier} $args SQL; } diff --git a/lib/Traits/CanQueryWordPressDatabase.php b/lib/Traits/CanQueryWordPressDatabase.php index 155188e..143d530 100644 --- a/lib/Traits/CanQueryWordPressDatabase.php +++ b/lib/Traits/CanQueryWordPressDatabase.php @@ -82,7 +82,7 @@ protected function wpdbInsert(Table $table, array $data): array global $wpdb; if (empty($data)) { - $inserted = $wpdb->query('INSERT INTO ' . $table->getName() . '() VALUES ();'); + $inserted = $wpdb->query($wpdb->prepare('INSERT INTO %i() VALUES ();', $table->getName())); } else { $inserted = $wpdb->insert($table->getName(), $data, $this->getFormats($data)); }