PluginProbe
Meow Gallery / 5.5.5
Meow Gallery v5.5.5
5.5.5 5.5.4 5.5.3 5.5.2 5.5.1 5.5.0 5.4.9 5.4.8 5.4.7 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 All 157 releases
← All changes | classes/rest.php +73 -7 5.5.45.5.5 View file →
@@ -4,8 +4,15 @@
4 4 {
5 5 private $core;
6 6 private $namespace = 'meow-gallery/v1';
7 7
8 + // Gallery attributes that decide *which* media a gallery shows. They must never be taken from
9 + // an untrusted request: see rest_load_gallery_collection().
10 + private static $source_atts = [
11 + 'collection', 'id', 'ids', 'include', 'tags', 'posts', 'latest_posts', 'attachments',
12 + 'rml', 'wplr-collection', 'meow',
13 + ];
14 +
8 15 public function __construct( $core ) {
9 16 $this->core = $core;
10 17
11 18 // FOR DEBUG
@@ -120,9 +127,9 @@
120 127
121 128 // Gallery
122 129 register_rest_route( $this->namespace, '/images/', array(
123 130 'methods' => 'POST',
124 - 'permission_callback' => '__return_true',
131 + 'permission_callback' => array( $this, 'can_load_images' ),
125 132 'callback' => array( $this, 'rest_images' )
126 133 ) );
127 134
128 135 register_rest_route( $this->namespace, '/fetch_posts', array(
@@ -136,8 +143,18 @@
136 143 )
137 144 ) );
138 145 }
139 146
147 + // The /images/ route feeds the infinite scroll and nothing else: when it is off (the default,
148 + // and always in the free version) the gallery is rendered whole and the front-end never calls
149 + // this. It has to stay open to visitors when infinite scroll IS on, but leaving it open
150 + // everywhere exposed the title, caption and URL of any attachment ID, including attachments of
151 + // posts that are not published.
152 + public function can_load_images() {
153 + $infinite = class_exists( 'MeowPro_MGL_Core' ) && Meow_MGL_Core::get_plugin_option( 'infinite', false );
154 + return apply_filters( 'mgl_allow_load_images', (bool) $infinite );
155 + }
156 +
140 157 function preview( WP_REST_Request $request ) {
141 158 $params = $request->get_body( );
142 159 $params = json_decode( $params );
143 160 $params->ids = implode( ',', $params->ids );
@@ -147,9 +164,9 @@
147 164 unset( $atts['full'] );
148 165
149 166 $is_collection = isset( $atts['collection'] ) && !empty( $atts['collection'] );
150 167 if ( $is_collection ) {
151 - $html = do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' );
168 + $html = $this->core->render_collection( $atts['collection'] );
152 169 $counts = [ 'total' => 0, 'shown' => 0 ];
153 170 } else {
154 171 $this->core->last_preview_counts = [ 'total' => 0, 'shown' => 0 ];
155 172 if ( $full ) {
@@ -170,11 +187,12 @@
170 187
171 188 function rest_load_gallery_collection( $request ) {
172 189 try {
173 190 $params = $request->get_json_params( );
174 - $gallery_id = $params['id'];
175 - $search_slug = $params['search_slug'];
176 - $gallery_atts = $params['gallery_atts'];
191 + $gallery_id = $params['id'] ?? '';
192 + $search_slug = $params['search_slug'] ?? '';
193 + $gallery_atts = $params['gallery_atts'] ?? array();
194 + $gallery_atts = is_array( $gallery_atts ) ? $gallery_atts : array();
177 195
178 196 $key = [
179 197 'gallery_id' => 'id',
180 198 'wplr_collection_id' => 'wplr-collection',
@@ -180,8 +198,26 @@
180 198 'wplr_collection_id' => 'wplr-collection',
181 199 'rml' => 'rml',
182 200 ];
183 201
202 + // This route is public (visitors open galleries from a collection), so everything it
203 + // receives is untrusted. The gallery to render is decided by 'search_slug' + 'id'
204 + // only: the caller-supplied attributes are stripped of anything that could point the
205 + // gallery at other content. Without this, 'collection' could be used to inject
206 + // arbitrary shortcodes (reported by JunHee CHO, 2026-09).
207 + if ( !isset( $key[ $search_slug ] ) ) {
208 + return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Unknown gallery source.', MGL_DOMAIN ) ], 400 );
209 + }
210 + $gallery_atts = array_diff_key( $gallery_atts, array_flip( self::$source_atts ) );
211 +
212 + // The RML source is a folder path, the others are identifiers.
213 + if ( $search_slug !== 'rml' ) {
214 + $gallery_id = Meow_MGL_Core::sanitize_id( $gallery_id );
215 + if ( $gallery_id === '' ) {
216 + return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Invalid gallery ID.', MGL_DOMAIN ) ], 400 );
217 + }
218 + }
219 +
184 220 $shortcode_atts = array();
185 221 $shortcode_atts[ $key[$search_slug] ] = $gallery_id;
186 222 $shortcode_atts = [...$shortcode_atts, ...$gallery_atts];
187 223
@@ -626,8 +662,33 @@
626 662 'data' => $this->core->get_gallery_images( $image_ids, $atts, $layout, $size )
627 663 ], 200 );
628 664 }
629 665
666 + // Applies WordPress's own visibility rules to a raw posts query: published posts for everyone,
667 + // other people's drafts only with edit_others_posts, other people's private posts only with
668 + // read_private_posts, and your own in both cases. 'upload_files' (the capability gating this
669 + // REST controller) is held by Authors, who must not see the whole site's unpublished content.
670 + private function get_post_status_clause( $alias = 'p', $post_type = 'post' ) {
671 + global $wpdb;
672 +
673 + $post_type_object = get_post_type_object( $post_type );
674 + $read_private_cap = $post_type_object ? $post_type_object->cap->read_private_posts : 'read_private_posts';
675 + $edit_others_cap = $post_type_object ? $post_type_object->cap->edit_others_posts : 'edit_others_posts';
676 +
677 + $user_id = get_current_user_id();
678 + $clause = "AND ( $alias.post_status = 'publish'";
679 +
680 + $clause .= current_user_can( $read_private_cap )
681 + ? " OR $alias.post_status = 'private'"
682 + : $wpdb->prepare( " OR ( $alias.post_status = 'private' AND $alias.post_author = %d )", $user_id );
683 +
684 + $clause .= current_user_can( $edit_others_cap )
685 + ? " OR $alias.post_status = 'draft'"
686 + : $wpdb->prepare( " OR ( $alias.post_status = 'draft' AND $alias.post_author = %d )", $user_id );
687 +
688 + return $clause . " ) ";
689 + }
690 +
630 691 function rest_fetch_posts( $request ) {
631 692 try {
632 693 $params = $request->get_json_params();
633 694 $search = isset($params['search']) ? $params['search'] : '';
@@ -642,8 +703,13 @@
642 703 $searchPlaceholder,
643 704 $searchPlaceholder
644 705 ) : '';
645 706
707 + // The same clause is used by both queries on purpose: the search also matches
708 + // post_content, so a count taken over a wider set than the rows would let a user probe
709 + // the body of posts they cannot read (reported by Kaan Ă–zbek, 2026-09).
710 + $where_status_clause = $this->get_post_status_clause( 'p' );
711 +
646 712 $posts = $wpdb->get_results(
647 713 $wpdb->prepare(
648 714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
649 715 FROM $wpdb->posts p
@@ -648,9 +714,9 @@
648 714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
649 715 FROM $wpdb->posts p
650 716 LEFT JOIN $wpdb->users u ON p.post_author = u.ID
651 717 WHERE p.post_type = 'post'
652 - AND p.post_status IN ('publish', 'draft', 'private')
718 + $where_status_clause
653 719 $where_search_clause
654 720 ORDER BY p.post_date DESC
655 721 LIMIT %d, %d",
656 722 $offset,
@@ -662,9 +728,9 @@
662 728 $posts_count = (int)$wpdb->get_var(
663 729 "SELECT COUNT(*)
664 730 FROM $wpdb->posts p
665 731 WHERE p.post_type = 'post'
666 - AND p.post_status IN ('publish', 'draft', 'private')
732 + $where_status_clause
667 733 $where_search_clause"
668 734 );
669 735
670 736 $data = array_map(function($post) {