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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user