Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 91 additions & 4 deletions includes/class-pattern-builder-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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' ) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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'] );
Expand Down
5 changes: 4 additions & 1 deletion includes/class-pattern-builder-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
26 changes: 17 additions & 9 deletions includes/class-pattern-builder-security.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down