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:
@@ -162,6 +162,7 @@ class TMDO_CLI {
|
|||||||
// v2.1.2 doctor column-drift check: compare DB columns vs Schema_Registry declared columns.
|
// v2.1.2 doctor column-drift check: compare DB columns vs Schema_Registry declared columns.
|
||||||
$declared = array_keys( $registry->get_hot_columns( $pt ) );
|
$declared = array_keys( $registry->get_hot_columns( $pt ) );
|
||||||
$existing = array();
|
$existing = array();
|
||||||
|
self::assert_safe_table_name( $t );
|
||||||
if ( TMDO_IS_MYSQL ) {
|
if ( TMDO_IS_MYSQL ) {
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
|
// 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}`" );
|
$rows = $wpdb->get_col( "SHOW COLUMNS FROM `{$t}`" );
|
||||||
@@ -914,6 +915,7 @@ class TMDO_CLI {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self::assert_safe_table_name( $full_table );
|
||||||
$total_rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$full_table}`" ); // phpcs:ignore WordPress.DB
|
$total_rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$full_table}`" ); // phpcs:ignore WordPress.DB
|
||||||
|
|
||||||
// Use partner-supplied callback when available.
|
// Use partner-supplied callback when available.
|
||||||
@@ -1318,6 +1320,22 @@ class TMDO_CLI {
|
|||||||
|
|
||||||
// ── Private helpers ───────────────────────────────────────────────────
|
// ── 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.
|
* Format the HPCT status string for display.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -38,6 +38,48 @@ class TMDO_Installer {
|
|||||||
*/
|
*/
|
||||||
private const SCHEMA_VERSION = '2.1.0';
|
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 ────────────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -958,6 +1000,7 @@ class TMDO_Installer {
|
|||||||
if ( method_exists( $wpdb, 'hide_errors' ) ) {
|
if ( method_exists( $wpdb, 'hide_errors' ) ) {
|
||||||
$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
|
// 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}" );
|
$ok = $wpdb->query( "ALTER TABLE `{$table}` ADD COLUMN `{$safe_name}` {$col_type}" );
|
||||||
if ( $prev_show && method_exists( $wpdb, 'show_errors' ) ) {
|
if ( $prev_show && method_exists( $wpdb, 'show_errors' ) ) {
|
||||||
@@ -1032,6 +1075,7 @@ class TMDO_Installer {
|
|||||||
|
|
||||||
foreach ( $columns as $col_name => $col_type ) {
|
foreach ( $columns as $col_name => $col_type ) {
|
||||||
$safe_name = sanitize_key( $col_name );
|
$safe_name = sanitize_key( $col_name );
|
||||||
|
self::validate_col_type( $col_type, $safe_name );
|
||||||
$col_defs .= " {$safe_name} {$col_type},\n";
|
$col_defs .= " {$safe_name} {$col_type},\n";
|
||||||
|
|
||||||
$base_type = strtolower( strtok( $col_type, '(' ) );
|
$base_type = strtolower( strtok( $col_type, '(' ) );
|
||||||
|
|||||||
Reference in New Issue
Block a user