@@ -55,13 +55,18 @@ public function get_posts( \WP_REST_Request $request ) {
5555 return new \WP_REST_Response ( '' );
5656 }
5757
58+ $ page_number = absint ( $ request ['page_number ' ] );
59+ if ( $ page_number > 100 ) {
60+ return new \WP_REST_Response ( '' );
61+ }
62+
5863 $ query_args = $ request ->get_body ();
5964 $ args = json_decode ( $ query_args , true );
60-
61- $ per_page = get_option ( 'posts_per_page ' );
65+ $ per_page = get_option ( 'posts_per_page ' );
6266 if ( $ per_page > 100 ) {
6367 $ per_page = 100 ;
6468 }
69+ $ args = $ this ->sanitize_infinite_scroll_query_args ( is_array ( $ args ) ? $ args : array () );
6570
6671 /**
6772 * If homepage is set to 'A static page', there will be a parameter inside the query named 'pagename'.
@@ -73,24 +78,17 @@ public function get_posts( \WP_REST_Request $request ) {
7378 }
7479
7580 $ 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 ' ;
81+ $ args ['paged ' ] = $ page_number ;
8482
8583 if ( ! empty ( $ request ['lang ' ] ) ) {
8684 if ( defined ( 'POLYLANG_VERSION ' ) ) {
87- $ args ['lang ' ] = $ request ['lang ' ];
85+ $ args ['lang ' ] = sanitize_text_field ( $ request ['lang ' ] ) ;
8886 }
8987
9088 if ( defined ( 'ICL_SITEPRESS_VERSION ' ) ) {
9189 global $ sitepress ;
9290 if ( gettype ( $ sitepress ) === 'object ' && method_exists ( $ sitepress , 'switch_lang ' ) ) {
93- $ sitepress ->switch_lang ( $ request ['lang ' ] );
91+ $ sitepress ->switch_lang ( sanitize_text_field ( $ request ['lang ' ] ) );
9492 }
9593 }
9694 }
@@ -302,6 +300,108 @@ public function render_post_navigation() {
302300 echo '</div> ' ;
303301 }
304302
303+ /**
304+ * Sanitize query arguments for infinite scroll to prevent query manipulation.
305+ *
306+ * This method implements a strict allowlist approach to prevent:
307+ * - Expensive database queries (DoS risk via meta_query, tax_query, etc.)
308+ * - Exposure of unintended content types
309+ * - Manipulation of query parameters by anonymous users
310+ *
311+ * @param array<string, mixed> $args Raw query arguments from client request.
312+ *
313+ * @return array<string, mixed> Sanitized query arguments safe for WP_Query.
314+ */
315+ private function sanitize_infinite_scroll_query_args ( $ args ) {
316+ // Define allowlist of safe query parameters for public infinite scroll.
317+ $ allowed_keys = array (
318+ 'category_name ' ,
319+ 'tag ' ,
320+ 's ' ,
321+ 'order ' ,
322+ 'orderby ' ,
323+ 'author ' ,
324+ 'author_name ' ,
325+ 'year ' ,
326+ 'monthnum ' ,
327+ 'day ' ,
328+ );
329+
330+ $ sanitized = array ();
331+ foreach ( $ allowed_keys as $ key ) {
332+ if ( isset ( $ args [ $ key ] ) ) {
333+ $ sanitized [ $ key ] = $ args [ $ key ];
334+ }
335+ }
336+
337+ if ( isset ( $ sanitized ['category_name ' ] ) ) {
338+ $ sanitized ['category_name ' ] = sanitize_text_field ( $ sanitized ['category_name ' ] );
339+ }
340+ if ( isset ( $ sanitized ['tag ' ] ) ) {
341+ $ sanitized ['tag ' ] = sanitize_text_field ( $ sanitized ['tag ' ] );
342+ }
343+ if ( isset ( $ sanitized ['s ' ] ) ) {
344+ $ sanitized ['s ' ] = sanitize_text_field ( $ sanitized ['s ' ] );
345+ }
346+ if ( isset ( $ sanitized ['order ' ] ) ) {
347+ $ sanitized ['order ' ] = in_array ( strtoupper ( $ sanitized ['order ' ] ), array ( 'ASC ' , 'DESC ' ), true ) ? $ sanitized ['order ' ] : 'DESC ' ;
348+ }
349+ if ( isset ( $ sanitized ['orderby ' ] ) ) {
350+ $ safe_orderby = array ( 'date ' , 'title ' , 'author ' , 'modified ' , 'comment_count ' , 'rand ' );
351+ $ sanitized ['orderby ' ] = in_array ( $ sanitized ['orderby ' ], $ safe_orderby , true ) ? $ sanitized ['orderby ' ] : 'date ' ;
352+ }
353+ if ( isset ( $ sanitized ['author ' ] ) ) {
354+ $ sanitized ['author ' ] = absint ( $ sanitized ['author ' ] );
355+ }
356+ if ( isset ( $ sanitized ['author_name ' ] ) ) {
357+ $ sanitized ['author_name ' ] = sanitize_user ( $ sanitized ['author_name ' ] );
358+ }
359+ if ( isset ( $ sanitized ['year ' ] ) ) {
360+ $ sanitized ['year ' ] = absint ( $ sanitized ['year ' ] );
361+ }
362+ if ( isset ( $ sanitized ['monthnum ' ] ) ) {
363+ $ sanitized ['monthnum ' ] = absint ( $ sanitized ['monthnum ' ] );
364+ }
365+ if ( isset ( $ sanitized ['day ' ] ) ) {
366+ $ sanitized ['day ' ] = absint ( $ sanitized ['day ' ] );
367+ }
368+
369+ $ post_type = ! empty ( $ args ['post_type ' ] ) ? $ args ['post_type ' ] : 'post ' ;
370+ $ post_type_obj = get_post_type_object ( $ post_type );
371+
372+ // Only allow if post type exists and is publicly queryable.
373+ if ( $ post_type_obj && $ post_type_obj ->publicly_queryable ) {
374+ $ sanitized ['post_type ' ] = $ post_type ;
375+ } else {
376+ $ sanitized ['post_type ' ] = 'post ' ;
377+ }
378+
379+ // Explicitly unset dangerous query args that could be smuggled in.
380+ $ dangerous_keys = array_flip (
381+ array (
382+ 'meta_query ' ,
383+ 'meta_key ' ,
384+ 'meta_value ' ,
385+ 'meta_value_num ' ,
386+ 'meta_compare ' ,
387+ 'tax_query ' ,
388+ 'fields ' ,
389+ 'post__in ' ,
390+ 'post__not_in ' ,
391+ 'post_parent ' ,
392+ 'post_parent__in ' ,
393+ 'post_parent__not_in ' ,
394+ )
395+ );
396+ $ sanitized = array_diff_key ( $ sanitized , $ dangerous_keys );
397+
398+ // Force safe defaults for core query behavior.
399+ $ sanitized ['post_status ' ] = 'publish ' ;
400+ $ sanitized ['ignore_sticky_posts ' ] = 1 ;
401+
402+ return $ sanitized ;
403+ }
404+
305405 /**
306406 * Go to page option is enabled
307407 *
0 commit comments