From d040400da8f328632fd16f2191427da0d0ce46e2 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 31 Jul 2026 05:21:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(rest):=20=E5=85=AC=E9=96=8B=E7=AB=AF?= =?UTF-8?q?=E9=BB=9E=E5=8A=A0=E4=B8=8A=20show=5Fin=5Frest=20=E6=AC=84?= =?UTF-8?q?=E4=BD=8D=20allowlist=EF=BC=88A5/A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY --- includes/class-tmdo-rest-api.php | 41 +++++++++++++++-- includes/class-tmdo-schema-registry.php | 58 ++++++++++++++++++++----- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/includes/class-tmdo-rest-api.php b/includes/class-tmdo-rest-api.php index e532af2..08c6803 100644 --- a/includes/class-tmdo-rest-api.php +++ b/includes/class-tmdo-rest-api.php @@ -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 diff --git a/includes/class-tmdo-schema-registry.php b/includes/class-tmdo-schema-registry.php index 9b1fec5..971b552 100644 --- a/includes/class-tmdo-schema-registry.php +++ b/includes/class-tmdo-schema-registry.php @@ -94,22 +94,24 @@ 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( $config, array( - 'post_type' => '', - 'entity_type' => '', - 'meta_key' => '', - 'zone' => 'hot', - 'data_type' => 'longtext', - 'column' => '', - 'indexed' => false, - 'ttl' => null, - 'cache_group' => '', - 'cache_ttl' => HOUR_IN_SECONDS, - 'provider' => $provider, + 'post_type' => '', + 'entity_type' => '', + 'meta_key' => '', + 'zone' => 'hot', + 'data_type' => 'longtext', + 'column' => '', + 'indexed' => false, + 'ttl' => null, + '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. *