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
+38 -3
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.
$items = array_map(
function ( array $row ) use ( $post_type ): array {
// 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, $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