diff --git a/includes/class-pattern-builder-api.php b/includes/class-pattern-builder-api.php index cda875a..0728dc9 100644 --- a/includes/class-pattern-builder-api.php +++ b/includes/class-pattern-builder-api.php @@ -112,8 +112,8 @@ public function write_permission_callback( $request ) { */ public function process_theme_patterns( WP_REST_Request $request ): WP_REST_Response { - $localize = $request->get_param( 'localize' ) === 'true'; - $import_images = $request->get_param( 'importImages' ) !== 'false'; + $localize = sanitize_text_field( $request->get_param( 'localize' ) ) === 'true'; + $import_images = sanitize_text_field( $request->get_param( 'importImages' ) ) !== 'false'; $options = array( 'localize' => $localize, @@ -396,6 +396,18 @@ function handle_hijack_block_update( $response, $handler, $request ) { $updated_pattern = json_decode( $request->get_body(), true ); + // Validate JSON decode was successful + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new WP_Error( + 'invalid_json', + __( 'Invalid JSON in request body.', 'pattern-builder' ), + array( 'status' => 400 ) + ); + } + + // Sanitize the input data + $updated_pattern = $this->sanitize_pattern_input( $updated_pattern ); + $convert_user_pattern_to_theme_pattern = false; if ( $post->post_type === 'wp_block' ) { @@ -472,12 +484,12 @@ function handle_hijack_block_update( $response, $handler, $request ) { // Check configuration options via query parameters $options = array(); - $localize_param = $request->get_param( 'patternBuilderLocalize' ); + $localize_param = sanitize_text_field( $request->get_param( 'patternBuilderLocalize' ) ); if ( $localize_param === 'true' ) { $options['localize'] = true; } - $import_images_param = $request->get_param( 'patternBuilderImportImages' ); + $import_images_param = sanitize_text_field( $request->get_param( 'patternBuilderImportImages' ) ); if ( $import_images_param === 'false' ) { $options['import_images'] = false; } else { @@ -497,12 +509,87 @@ function handle_hijack_block_update( $response, $handler, $request ) { return $response; } + /** + * Sanitizes pattern input data to prevent XSS and ensure data integrity. + * + * @param array $input The input data to sanitize. + * @return array Sanitized input data. + */ + private function sanitize_pattern_input( $input ) { + if ( ! is_array( $input ) ) { + return array(); + } + + $sanitized = array(); + + // Sanitize text fields + if ( isset( $input['title'] ) ) { + $sanitized['title'] = sanitize_text_field( $input['title'] ); + } + + if ( isset( $input['excerpt'] ) ) { + $sanitized['excerpt'] = sanitize_textarea_field( $input['excerpt'] ); + } + + // Sanitize content - allow HTML but sanitize it + if ( isset( $input['content'] ) ) { + $sanitized['content'] = wp_kses_post( $input['content'] ); + } + + // Sanitize source field + if ( isset( $input['source'] ) ) { + $sanitized['source'] = in_array( $input['source'], array( 'theme', 'user' ), true ) ? $input['source'] : 'user'; + } + + // Sanitize sync status + if ( isset( $input['wp_pattern_sync_status'] ) ) { + $sanitized['wp_pattern_sync_status'] = in_array( $input['wp_pattern_sync_status'], array( 'synced', 'unsynced' ), true ) ? $input['wp_pattern_sync_status'] : 'unsynced'; + } + + // Sanitize inserter setting + if ( isset( $input['wp_pattern_inserter'] ) ) { + $sanitized['wp_pattern_inserter'] = in_array( $input['wp_pattern_inserter'], array( 'yes', 'no' ), true ) ? $input['wp_pattern_inserter'] : 'yes'; + } + + // Sanitize array fields + $array_fields = array( 'wp_pattern_block_types', 'wp_pattern_post_types', 'wp_pattern_template_types' ); + foreach ( $array_fields as $field ) { + if ( isset( $input[ $field ] ) ) { + if ( is_array( $input[ $field ] ) ) { + $sanitized[ $field ] = array_map( 'sanitize_text_field', $input[ $field ] ); + } elseif ( is_string( $input[ $field ] ) ) { + // Handle comma-separated strings + $values = explode( ',', $input[ $field ] ); + $sanitized[ $field ] = array_map( 'sanitize_text_field', $values ); + } + } + } + + // Pass through other fields that don't need sanitization but need to be preserved + $passthrough_fields = array( 'id', 'date', 'date_gmt', 'modified', 'modified_gmt', 'status', 'type' ); + foreach ( $passthrough_fields as $field ) { + if ( isset( $input[ $field ] ) ) { + $sanitized[ $field ] = $input[ $field ]; + } + } + + return $sanitized; + } + /** * When anything is saved any wp:block that references a theme pattern is converted to a wp:pattern block instead. */ public function handle_block_to_pattern_conversion( $response, $handler, $request ) { if ( $request->get_method() === 'PUT' || $request->get_method() === 'POST' ) { $body = json_decode( $request->get_body(), true ); + + // Validate JSON decode was successful + if ( json_last_error() !== JSON_ERROR_NONE ) { + return $response; // Return original response if JSON is invalid + } + + // Sanitize the input data + $body = $this->sanitize_pattern_input( $body ); if ( isset( $body['content'] ) ) { // parse the content string into blocks $blocks = parse_blocks( $body['content'] ); diff --git a/includes/class-pattern-builder-controller.php b/includes/class-pattern-builder-controller.php index a77e2ca..d1f05c4 100644 --- a/includes/class-pattern-builder-controller.php +++ b/includes/class-pattern-builder-controller.php @@ -154,7 +154,10 @@ public function update_theme_pattern( Abstract_Pattern $pattern, $options = arra $this->update_theme_pattern_file( $pattern ); // rebuild the pattern from the file (so that the content has no PHP tags) - $pattern = Abstract_Pattern::from_file( $this->get_pattern_filepath( $pattern ) ); + $filepath = $this->get_pattern_filepath( $pattern ); + if ( $filepath ) { + $pattern = Abstract_Pattern::from_file( $filepath ); + } $post_id = wp_update_post( array( diff --git a/includes/class-pattern-builder-security.php b/includes/class-pattern-builder-security.php index 2c6302d..f6bcb49 100644 --- a/includes/class-pattern-builder-security.php +++ b/includes/class-pattern-builder-security.php @@ -24,15 +24,23 @@ class Pattern_Builder_Security { * @return bool|WP_Error True if path is valid, WP_Error otherwise. */ public static function validate_file_path( $path, $allowed_dirs = array() ) { - // Normalize the path to prevent traversal attempts. - $path = wp_normalize_path( realpath( $path ) ); - - if ( false === $path ) { - return new WP_Error( - 'invalid_path', - __( 'Invalid file path provided.', 'pattern-builder' ), - array( 'status' => 400 ) - ); + // First normalize the path without realpath to handle non-existing files. + $normalized_path = wp_normalize_path( $path ); + + // If the file exists, use realpath for stronger validation. + if ( file_exists( $path ) ) { + $real_path = wp_normalize_path( realpath( $path ) ); + if ( false === $real_path ) { + return new WP_Error( + 'invalid_path', + __( 'Invalid file path provided.', 'pattern-builder' ), + array( 'status' => 400 ) + ); + } + $path = $real_path; + } else { + // For non-existing files, validate the normalized path. + $path = $normalized_path; } // Default to theme directory if no allowed directories specified.