From fa31a0527b722df0d8deaa97779d1bb39ab32e47 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 31 Jul 2026 05:18:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20DDL=20=E5=9E=8B=E5=88=A5?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=96=AE=20+=20CLI=20=E8=A1=A8=E5=90=8D?= =?UTF-8?q?=E5=AE=88=E8=A1=9B=EF=BC=88A7/A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TMDO_Installer::ALLOWED_COL_BASE_TYPES(19 型別)+ validate_col_type(), 在 ALTER TABLE ADD COLUMN 與 CREATE TABLE 組欄位前驗證:先前 partner plugin 經 Schema Registry 提供的 $col_type 直接拼進 DDL。 - TMDO_CLI::assert_safe_table_name(),守 doctor 的 SHOW COLUMNS / PRAGMA 與 benchmark 的 COUNT(*) 三處無法 prepare 的識別字。 對應 A v3.3.4。unit 379 / integration 398 GREEN Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY --- cli/class-tmdo-cli.php | 18 +++++++++++++ includes/class-tmdo-installer.php | 44 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/cli/class-tmdo-cli.php b/cli/class-tmdo-cli.php index c32b9a4..a8ae9fb 100644 --- a/cli/class-tmdo-cli.php +++ b/cli/class-tmdo-cli.php @@ -162,6 +162,7 @@ class TMDO_CLI { // v2.1.2 doctor column-drift check: compare DB columns vs Schema_Registry declared columns. $declared = array_keys( $registry->get_hot_columns( $pt ) ); $existing = array(); + self::assert_safe_table_name( $t ); if ( TMDO_IS_MYSQL ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared $rows = $wpdb->get_col( "SHOW COLUMNS FROM `{$t}`" ); @@ -914,6 +915,7 @@ class TMDO_CLI { continue; } + self::assert_safe_table_name( $full_table ); $total_rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$full_table}`" ); // phpcs:ignore WordPress.DB // Use partner-supplied callback when available. @@ -1318,6 +1320,22 @@ class TMDO_CLI { // ── Private helpers ─────────────────────────────────────────────────── + /** + * Reject table names that are not plain identifiers. + * + * Guards raw SQL that cannot use $wpdb->prepare() for table identifiers + * (SHOW COLUMNS, PRAGMA, COUNT(*) probes). All callers derive $table_name + * from TMDO_DB::table() or Custom_Table_Registry — developer-controlled, + * not from HTTP input — but this check prevents breakage if that ever changes. + * + * @param string $table_name Fully-qualified table name to validate. + */ + private static function assert_safe_table_name( string $table_name ): void { + if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $table_name ) ) { + WP_CLI::error( "Unsafe table name rejected: '{$table_name}'" ); + } + } + /** * Format the HPCT status string for display. * diff --git a/includes/class-tmdo-installer.php b/includes/class-tmdo-installer.php index fd434f4..12353b7 100644 --- a/includes/class-tmdo-installer.php +++ b/includes/class-tmdo-installer.php @@ -38,6 +38,48 @@ class TMDO_Installer { */ private const SCHEMA_VERSION = '2.1.0'; + /** Allowed SQL base types for hot-zone column definitions. */ + private const ALLOWED_COL_BASE_TYPES = array( + 'bigint', + 'int', + 'tinyint', + 'smallint', + 'mediumint', + 'decimal', + 'float', + 'double', + 'varchar', + 'char', + 'text', + 'longtext', + 'mediumtext', + 'datetime', + 'date', + 'timestamp', + 'json', + ); + + /** + * Assert that a column type string starts with an allowed SQL base type. + * + * Prevents rogue partner plugins from injecting arbitrary DDL via Schema Registry. + * + * @param string $col_type Full column definition, e.g. "DECIMAL(10,2) NOT NULL DEFAULT '0'". + * @param string $col_name Column name (for error context). + * @throws \InvalidArgumentException When the base type is not in the allowlist. + */ + private static function validate_col_type( string $col_type, string $col_name ): void { + $base = strtolower( strtok( trim( $col_type ), " \t(" ) ); + if ( ! in_array( $base, self::ALLOWED_COL_BASE_TYPES, true ) ) { + // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never rendered to HTML. + throw new \InvalidArgumentException( + "TMDO_Installer: disallowed column type '{$base}' for column '{$col_name}'. " . + 'Allowed: ' . implode( ', ', self::ALLOWED_COL_BASE_TYPES ) + ); + // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped + } + } + // ── Public API ──────────────────────────────────────────────────────── /** @@ -958,6 +1000,7 @@ class TMDO_Installer { if ( method_exists( $wpdb, 'hide_errors' ) ) { $wpdb->hide_errors(); } + self::validate_col_type( $col_type, $safe_name ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared $ok = $wpdb->query( "ALTER TABLE `{$table}` ADD COLUMN `{$safe_name}` {$col_type}" ); if ( $prev_show && method_exists( $wpdb, 'show_errors' ) ) { @@ -1032,6 +1075,7 @@ class TMDO_Installer { foreach ( $columns as $col_name => $col_type ) { $safe_name = sanitize_key( $col_name ); + self::validate_col_type( $col_type, $safe_name ); $col_defs .= " {$safe_name} {$col_type},\n"; $base_type = strtolower( strtok( $col_type, '(' ) );