@@ -55,13 +55,14 @@ public function get_posts( \WP_REST_Request $request ) {
5555 return new \WP_REST_Response ( '' );
5656 }
5757
58- $ query_args = $ request-> get_body ( );
59- $ args = json_decode ( $ query_args , true );
60-
61- $ per_page = get_option ( 'posts_per_page ' );
58+ $ page_number = absint ( $ request[ ' page_number ' ] );
59+ $ query_args = $ request -> get_body ( );
60+ $ args = json_decode ( $ query_args , true );
61+ $ per_page = get_option ( 'posts_per_page ' );
6262 if ( $ per_page > 100 ) {
6363 $ per_page = 100 ;
6464 }
65+ $ args = $ this ->sanitize_infinite_scroll_query_args ( is_array ( $ args ) ? $ args : array () );
6566
6667 /**
6768 * If homepage is set to 'A static page', there will be a parameter inside the query named 'pagename'.
@@ -73,24 +74,17 @@ public function get_posts( \WP_REST_Request $request ) {
7374 }
7475
7576 $ args ['posts_per_page ' ] = $ per_page ;
76-
77- if ( empty ( $ args ['post_type ' ] ) ) {
78- $ args ['post_type ' ] = 'post ' ;
79- }
80-
81- $ args ['paged ' ] = $ request ['page_number ' ];
82- $ args ['ignore_sticky_posts ' ] = 1 ;
83- $ args ['post_status ' ] = 'publish ' ;
77+ $ args ['paged ' ] = $ page_number ;
8478
8579 if ( ! empty ( $ request ['lang ' ] ) ) {
8680 if ( defined ( 'POLYLANG_VERSION ' ) ) {
87- $ args ['lang ' ] = $ request ['lang ' ];
81+ $ args ['lang ' ] = sanitize_text_field ( $ request ['lang ' ] ) ;
8882 }
8983
9084 if ( defined ( 'ICL_SITEPRESS_VERSION ' ) ) {
9185 global $ sitepress ;
9286 if ( gettype ( $ sitepress ) === 'object ' && method_exists ( $ sitepress , 'switch_lang ' ) ) {
93- $ sitepress ->switch_lang ( $ request ['lang ' ] );
87+ $ sitepress ->switch_lang ( sanitize_text_field ( $ request ['lang ' ] ) );
9488 }
9589 }
9690 }
@@ -302,6 +296,109 @@ public function render_post_navigation() {
302296 echo '</div> ' ;
303297 }
304298
299+ /**
300+ * Sanitize query arguments for infinite scroll to prevent query manipulation.
301+ *
302+ * This method implements a strict allowlist approach to prevent:
303+ * - Expensive database queries (DoS risk via meta_query, tax_query, etc.)
304+ * - Exposure of unintended content types
305+ * - Manipulation of query parameters by anonymous users
306+ *
307+ * @param array<string, mixed> $args Raw query arguments from client request.
308+ *
309+ * @return array<string, mixed> Sanitized query arguments safe for WP_Query.
310+ */
311+ private function sanitize_infinite_scroll_query_args ( $ args ) {
312+ // Define allowlist of safe query parameters for public infinite scroll.
313+ $ allowed_keys = array (
314+ 'category_name ' ,
315+ 'tag ' ,
316+ 's ' ,
317+ 'order ' ,
318+ 'orderby ' ,
319+ 'author ' ,
320+ 'author_name ' ,
321+ 'year ' ,
322+ 'monthnum ' ,
323+ 'day ' ,
324+ );
325+
326+ $ sanitized = array ();
327+ foreach ( $ allowed_keys as $ key ) {
328+ if ( isset ( $ args [ $ key ] ) ) {
329+ $ sanitized [ $ key ] = $ args [ $ key ];
330+ }
331+ }
332+
333+ if ( isset ( $ sanitized ['category_name ' ] ) ) {
334+ $ sanitized ['category_name ' ] = sanitize_text_field ( $ sanitized ['category_name ' ] );
335+ }
336+ if ( isset ( $ sanitized ['tag ' ] ) ) {
337+ $ sanitized ['tag ' ] = sanitize_text_field ( $ sanitized ['tag ' ] );
338+ }
339+ if ( isset ( $ sanitized ['s ' ] ) ) {
340+ $ sanitized ['s ' ] = sanitize_text_field ( $ sanitized ['s ' ] );
341+ }
342+ if ( isset ( $ sanitized ['order ' ] ) ) {
343+ $ order_upper = is_string ( $ sanitized ['order ' ] ) ? strtoupper ( $ sanitized ['order ' ] ) : '' ;
344+ $ sanitized ['order ' ] = in_array ( $ order_upper , array ( 'ASC ' , 'DESC ' ), true ) ? $ order_upper : 'DESC ' ;
345+ }
346+ if ( isset ( $ sanitized ['orderby ' ] ) ) {
347+ $ safe_orderby = array ( 'date ' , 'title ' , 'author ' , 'modified ' , 'comment_count ' );
348+ $ sanitized ['orderby ' ] = in_array ( $ sanitized ['orderby ' ], $ safe_orderby , true ) ? $ sanitized ['orderby ' ] : 'date ' ;
349+ }
350+ if ( isset ( $ sanitized ['author ' ] ) ) {
351+ $ sanitized ['author ' ] = absint ( $ sanitized ['author ' ] );
352+ }
353+ if ( isset ( $ sanitized ['author_name ' ] ) ) {
354+ $ sanitized ['author_name ' ] = sanitize_user ( $ sanitized ['author_name ' ] );
355+ }
356+ if ( isset ( $ sanitized ['year ' ] ) ) {
357+ $ sanitized ['year ' ] = absint ( $ sanitized ['year ' ] );
358+ }
359+ if ( isset ( $ sanitized ['monthnum ' ] ) ) {
360+ $ sanitized ['monthnum ' ] = absint ( $ sanitized ['monthnum ' ] );
361+ }
362+ if ( isset ( $ sanitized ['day ' ] ) ) {
363+ $ sanitized ['day ' ] = absint ( $ sanitized ['day ' ] );
364+ }
365+
366+ $ post_type = ( ! empty ( $ args ['post_type ' ] ) && is_string ( $ args ['post_type ' ] ) ) ? sanitize_key ( $ args ['post_type ' ] ) : 'post ' ;
367+ $ post_type_obj = get_post_type_object ( $ post_type );
368+
369+ // Only allow if post type exists and is publicly queryable.
370+ if ( $ post_type_obj && $ post_type_obj ->publicly_queryable ) {
371+ $ sanitized ['post_type ' ] = $ post_type ;
372+ } else {
373+ $ sanitized ['post_type ' ] = 'post ' ;
374+ }
375+
376+ // Explicitly unset dangerous query args that could be smuggled in.
377+ $ dangerous_keys = array_flip (
378+ array (
379+ 'meta_query ' ,
380+ 'meta_key ' ,
381+ 'meta_value ' ,
382+ 'meta_value_num ' ,
383+ 'meta_compare ' ,
384+ 'tax_query ' ,
385+ 'fields ' ,
386+ 'post__in ' ,
387+ 'post__not_in ' ,
388+ 'post_parent ' ,
389+ 'post_parent__in ' ,
390+ 'post_parent__not_in ' ,
391+ )
392+ );
393+ $ sanitized = array_diff_key ( $ sanitized , $ dangerous_keys );
394+
395+ // Force safe defaults for core query behavior.
396+ $ sanitized ['post_status ' ] = 'publish ' ;
397+ $ sanitized ['ignore_sticky_posts ' ] = 1 ;
398+
399+ return $ sanitized ;
400+ }
401+
305402 /**
306403 * Go to page option is enabled
307404 *
0 commit comments