feat(security): DDL 型別白名單 + CLI 表名守衛(A7/A8)

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
2026-07-31 05:18:55 +08:00
parent a0847a27bf
commit fa31a0527b
2 changed files with 62 additions and 0 deletions
+44
View File
@@ -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, '(' ) );