diff --git a/inc/views/pluggable/pagination.php b/inc/views/pluggable/pagination.php index a0e95f9cf5..c6f3671ce3 100644 --- a/inc/views/pluggable/pagination.php +++ b/inc/views/pluggable/pagination.php @@ -55,13 +55,18 @@ public function get_posts( \WP_REST_Request $request ) { return new \WP_REST_Response( '' ); } + $page_number = absint( $request['page_number'] ); + if ( $page_number > 100 ) { + return new \WP_REST_Response( '' ); + } + $query_args = $request->get_body(); $args = json_decode( $query_args, true ); - - $per_page = get_option( 'posts_per_page' ); + $per_page = get_option( 'posts_per_page' ); if ( $per_page > 100 ) { $per_page = 100; } + $args = $this->sanitize_infinite_scroll_query_args( is_array( $args ) ? $args : array() ); /** * 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 ) { } $args['posts_per_page'] = $per_page; - - if ( empty( $args['post_type'] ) ) { - $args['post_type'] = 'post'; - } - - $args['paged'] = $request['page_number']; - $args['ignore_sticky_posts'] = 1; - $args['post_status'] = 'publish'; + $args['paged'] = $page_number; if ( ! empty( $request['lang'] ) ) { if ( defined( 'POLYLANG_VERSION' ) ) { - $args['lang'] = $request['lang']; + $args['lang'] = sanitize_text_field( $request['lang'] ); } if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { global $sitepress; if ( gettype( $sitepress ) === 'object' && method_exists( $sitepress, 'switch_lang' ) ) { - $sitepress->switch_lang( $request['lang'] ); + $sitepress->switch_lang( sanitize_text_field( $request['lang'] ) ); } } } @@ -302,6 +300,108 @@ public function render_post_navigation() { echo ''; } + /** + * Sanitize query arguments for infinite scroll to prevent query manipulation. + * + * This method implements a strict allowlist approach to prevent: + * - Expensive database queries (DoS risk via meta_query, tax_query, etc.) + * - Exposure of unintended content types + * - Manipulation of query parameters by anonymous users + * + * @param array $args Raw query arguments from client request. + * + * @return array Sanitized query arguments safe for WP_Query. + */ + private function sanitize_infinite_scroll_query_args( $args ) { + // Define allowlist of safe query parameters for public infinite scroll. + $allowed_keys = array( + 'category_name', + 'tag', + 's', + 'order', + 'orderby', + 'author', + 'author_name', + 'year', + 'monthnum', + 'day', + ); + + $sanitized = array(); + foreach ( $allowed_keys as $key ) { + if ( isset( $args[ $key ] ) ) { + $sanitized[ $key ] = $args[ $key ]; + } + } + + if ( isset( $sanitized['category_name'] ) ) { + $sanitized['category_name'] = sanitize_text_field( $sanitized['category_name'] ); + } + if ( isset( $sanitized['tag'] ) ) { + $sanitized['tag'] = sanitize_text_field( $sanitized['tag'] ); + } + if ( isset( $sanitized['s'] ) ) { + $sanitized['s'] = sanitize_text_field( $sanitized['s'] ); + } + if ( isset( $sanitized['order'] ) ) { + $sanitized['order'] = in_array( strtoupper( $sanitized['order'] ), array( 'ASC', 'DESC' ), true ) ? $sanitized['order'] : 'DESC'; + } + if ( isset( $sanitized['orderby'] ) ) { + $safe_orderby = array( 'date', 'title', 'author', 'modified', 'comment_count', 'rand' ); + $sanitized['orderby'] = in_array( $sanitized['orderby'], $safe_orderby, true ) ? $sanitized['orderby'] : 'date'; + } + if ( isset( $sanitized['author'] ) ) { + $sanitized['author'] = absint( $sanitized['author'] ); + } + if ( isset( $sanitized['author_name'] ) ) { + $sanitized['author_name'] = sanitize_user( $sanitized['author_name'] ); + } + if ( isset( $sanitized['year'] ) ) { + $sanitized['year'] = absint( $sanitized['year'] ); + } + if ( isset( $sanitized['monthnum'] ) ) { + $sanitized['monthnum'] = absint( $sanitized['monthnum'] ); + } + if ( isset( $sanitized['day'] ) ) { + $sanitized['day'] = absint( $sanitized['day'] ); + } + + $post_type = ! empty( $args['post_type'] ) ? $args['post_type'] : 'post'; + $post_type_obj = get_post_type_object( $post_type ); + + // Only allow if post type exists and is publicly queryable. + if ( $post_type_obj && $post_type_obj->publicly_queryable ) { + $sanitized['post_type'] = $post_type; + } else { + $sanitized['post_type'] = 'post'; + } + + // Explicitly unset dangerous query args that could be smuggled in. + $dangerous_keys = array_flip( + array( + 'meta_query', + 'meta_key', + 'meta_value', + 'meta_value_num', + 'meta_compare', + 'tax_query', + 'fields', + 'post__in', + 'post__not_in', + 'post_parent', + 'post_parent__in', + 'post_parent__not_in', + ) + ); + $sanitized = array_diff_key( $sanitized, $dangerous_keys ); + + // Force safe defaults for core query behavior. + $sanitized['post_status'] = 'publish'; + $sanitized['ignore_sticky_posts'] = 1; + + return $sanitized; + } + /** * Go to page option is enabled *