fix(rest): 公開端點加上 show_in_rest 欄位 allowlist(A5/A6)

4 個 permission_callback => '__return_true' 的公開端點先前回傳 hot+cold
全部欄位,未註冊 show_in_rest 的私密欄位隨之外洩(A v3.3.1,CVSS 5.3)。

- Schema_Registry::register() 新增 show_in_rest 預設 true
- 新增 get_rest_visible_hot_columns() / get_rest_visible_cold_keys()
- get_listing() 以 array_intersect_key 過濾,保留 id / post_type,
  並提供 wpdo_rest_listing_visible_fields / tmdo_* filter 供調整
- listings_from_zone_a() 同樣以 visible_cols 過濾

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:21:10 +08:00
parent fa31a0527b
commit d040400da8
2 changed files with 85 additions and 14 deletions
+37 -2
View File
@@ -595,6 +595,35 @@ class TMDO_REST_API {
}
}
// Strip fields not marked show_in_rest => true in Schema_Registry.
// 'id' and 'post_type' are always retained. The filter below can add/remove
// fields on top of this default restriction.
$registry = TMDO_Schema_Registry::instance();
$visible_keys = array_fill_keys(
array_merge(
array( 'id', 'post_type' ),
$registry->get_rest_visible_hot_columns( $post_type ),
$registry->get_rest_visible_cold_keys( $post_type )
),
true
);
$data = array_intersect_key( $data, $visible_keys );
/**
* Filters the fields returned by GET /wpdo/v1/listings/{id}.
*
* Called after the default show_in_rest restriction. Use this hook to expose
* additional private fields to authenticated users, or to remove fields for
* specific consumers.
*
* @param array $data Field map (already restricted to show_in_rest=true fields).
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
*/
$data = (array) apply_filters( 'wpdo_rest_listing_visible_fields', $data, $post_id, $post_type );
/** This filter is documented above (tmdo_* is the forward-looking name). */
$data = (array) apply_filters( 'tmdo_rest_listing_visible_fields', $data, $post_id, $post_type );
return new WP_REST_Response( $data, 200 );
}
@@ -1389,14 +1418,20 @@ class TMDO_REST_API {
) ?: array();
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
// Rename post_id → id, drop internal columns.
// Rename post_id → id, drop internal columns, and restrict to show_in_rest fields.
$visible_cols = array_fill_keys(
TMDO_Schema_Registry::instance()->get_rest_visible_hot_columns( $post_type ),
true
);
$items = array_map(
function ( array $row ) use ( $post_type ): array {
function ( array $row ) use ( $post_type, $visible_cols ): array {
$item = array(
'id' => (int) $row['post_id'],
'post_type' => $post_type,
);
unset( $row['post_id'], $row['updated_at'] );
// Keep only columns with show_in_rest => true (or all if no registry entry).
$row = $visible_cols ? array_intersect_key( $row, $visible_cols ) : $row;
return array_merge( $item, $row );
},
$rows
+36
View File
@@ -94,6 +94,7 @@ class TMDO_Schema_Registry {
* - ttl (int) TTL in seconds (Zone B only, null = no expiry).
* - cache_group (string) Object cache group (Zone C only).
* - cache_ttl (int) Cache TTL in seconds (Zone C only).
* - show_in_rest (bool) Whether to expose via GET /wpdo/v1/listings. Default true.
*/
public function register( string $provider, array $config ): void {
$config = wp_parse_args(
@@ -110,6 +111,7 @@ class TMDO_Schema_Registry {
'cache_group' => '',
'cache_ttl' => HOUR_IN_SECONDS,
'provider' => $provider,
'show_in_rest' => true,
)
);
@@ -232,6 +234,40 @@ class TMDO_Schema_Registry {
return array_unique( $this->cold_fields[ $post_type ] ?? array() );
}
/**
* Returns hot column names for a post type where show_in_rest is true.
* Used by REST endpoints to restrict publicly exposed fields.
*
* @param string $post_type Post type.
* @return string[] Column names that are REST-visible.
*/
public function get_rest_visible_hot_columns( string $post_type ): array {
$visible = array();
foreach ( $this->fields as $field ) {
if ( 'hot' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) {
$visible[] = $field['column'];
}
}
return $visible;
}
/**
* Returns cold meta keys for a post type where show_in_rest is true.
* Used by REST endpoints to restrict publicly exposed fields.
*
* @param string $post_type Post type.
* @return string[] Meta keys that are REST-visible.
*/
public function get_rest_visible_cold_keys( string $post_type ): array {
$visible = array();
foreach ( $this->fields as $field ) {
if ( 'cold' === $field['zone'] && $field['post_type'] === $post_type && ! empty( $field['show_in_rest'] ) ) {
$visible[] = $field['meta_key'];
}
}
return array_unique( $visible );
}
/**
* Get warm field configuration.
*