From 08d0835c23d54faf8775142bb13b0ca029ba3a2d Mon Sep 17 00:00:00 2001 From: Ariful Hoque Date: Sun, 5 Jul 2026 11:56:25 +0600 Subject: [PATCH 1/2] fix: security hardening from 2026-07 audit Security-only, feature-preserving fixes for the free-plugin findings from the 2026-07 audit (tracked with the pro findings on weDevsOfficial/wpuf-pro#1622). - URL custom-field output: esc_url/esc_attr/esc_html (stored XSS) (weDevsOfficial/wpuf-pro#1608) - wpuf_clear_schedule_lock: require edit_others_posts + absint (missing authz) (weDevsOfficial/wpuf-pro#1609) - custom-field labels esc_html, embed URL esc_url_raw, transaction query strict in_array + absint (weDevsOfficial/wpuf-pro#1610) - repeat-field submissions: per-element sanitize on storage (weDevsOfficial/wpuf-pro#1620) - JSON import: allowlist post_type/post_status (mass-assignment) (weDevsOfficial/wpuf-pro#1621) - Settings API: replace deprecated create_function with a closure No functional behaviour change for legitimate flows. Co-Authored-By: Claude Fable 5 --- Lib/WeDevs_Settings_API.php | 4 +- includes/Admin/Admin_Tools.php | 26 ++++++++++--- includes/Traits/FieldableTrait.php | 8 +++- includes/class-frontend-render-form.php | 18 ++++++++- wpuf-functions.php | 50 +++++++++++++++---------- 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/Lib/WeDevs_Settings_API.php b/Lib/WeDevs_Settings_API.php index 31e8bcb86..544fe41eb 100644 --- a/Lib/WeDevs_Settings_API.php +++ b/Lib/WeDevs_Settings_API.php @@ -107,7 +107,9 @@ function admin_init() { if ( isset($section['desc']) && !empty($section['desc']) ) { $section['desc'] = '
' . $section['desc'] . '
'; - $callback = create_function('', 'echo "' . str_replace( '"', '\"', $section['desc'] ) . '";'); // phpcs:ignore + $callback = function() use ( $section ) { + echo wp_kses_post( $section['desc'] ); + }; } else if ( isset( $section['callback'] ) ) { $callback = $section['callback']; } else { diff --git a/includes/Admin/Admin_Tools.php b/includes/Admin/Admin_Tools.php index c51649b32..0a8c19a56 100644 --- a/includes/Admin/Admin_Tools.php +++ b/includes/Admin/Admin_Tools.php @@ -184,13 +184,29 @@ public static function import_json_file( $file ) { $errors = new WP_Error(); + $allowed_post_types = [ 'wpuf_forms', 'wpuf_profile' ]; + $allowed_post_statuses = [ 'publish', 'draft', 'pending' ]; + foreach ( $options as $key => $value ) { + // Allowlist post_type and post_status to prevent mass-assignment of an + // arbitrary post type/status through an imported JSON file. + $post_type = $value['post_data']['post_type'] ?? ''; + $post_status = $value['post_data']['post_status'] ?? ''; + + if ( ! in_array( $post_type, $allowed_post_types, true ) ) { + $post_type = 'wpuf_forms'; + } + + if ( ! in_array( $post_status, $allowed_post_statuses, true ) ) { + $post_status = 'publish'; + } + $generate_post = [ - 'post_title' => $value['post_data']['post_title'], - 'post_status' => $value['post_data']['post_status'], - 'post_type' => $value['post_data']['post_type'], - 'ping_status' => $value['post_data']['ping_status'], - 'comment_status' => $value['post_data']['comment_status'], + 'post_title' => $value['post_data']['post_title'] ?? '', + 'post_status' => $post_status, + 'post_type' => $post_type, + 'ping_status' => $value['post_data']['ping_status'] ?? '', + 'comment_status' => $value['post_data']['comment_status'] ?? '', ]; $post_id = wp_insert_post( $generate_post, true ); diff --git a/includes/Traits/FieldableTrait.php b/includes/Traits/FieldableTrait.php index d85bea091..fb4822777 100644 --- a/includes/Traits/FieldableTrait.php +++ b/includes/Traits/FieldableTrait.php @@ -903,9 +903,13 @@ public static function prepare_meta_fields( $meta_vars ) { } $meta_key_value[ $value['name'] ] = $rows; } else { - // Fallback to old logic for single-field repeaters or legacy structure + // Fallback to old logic for single-field repeaters or legacy structure. + // Sanitize each element at the storage layer (no output escaping so + // structured/multi-value data still round-trips). $meta_key_value[ $value['name'] ] = is_array( $repeater_value ) ? implode( - self::$separator, $repeater_value + self::$separator, array_map( function( $item ) { + return strip_shortcodes( sanitize_text_field( $item ) ); + }, $repeater_value ) ) : ''; } break; diff --git a/includes/class-frontend-render-form.php b/includes/class-frontend-render-form.php index fd745f74f..276062a52 100644 --- a/includes/class-frontend-render-form.php +++ b/includes/class-frontend-render-form.php @@ -783,8 +783,22 @@ public static function prepare_meta_fields( $meta_vars ) { $formatted_key = $parent_name . '_' . $i . '_' . $meta_key; if ( '' !== $meta_key && ! empty( $_POST[ $formatted_key ] ) ) { - $repeat_fields['fields'][ $formatted_key ] = wp_unslash( $_POST[ $formatted_key ] ); - $meta_key_value[ $formatted_key ] = wp_unslash( $_POST[ $formatted_key ] ); // phpcs:ignore WordPress.Security + // Storage-layer sanitization for repeat-field child values. Use the + // textarea sanitizer for textarea children (preserve newlines), + // otherwise the text-field sanitizer. Sanitize per element so + // multi-value repeat data still round-trips. + $repeat_sanitize_cb = ( isset( $value['input_type'] ) && 'textarea' === $value['input_type'] ) + ? 'sanitize_textarea_field' + : 'sanitize_text_field'; + + $raw_repeat_value = wp_unslash( $_POST[ $formatted_key ] ); // phpcs:ignore WordPress.Security.NonceVerification + + $sanitized_repeat_value = is_array( $raw_repeat_value ) + ? array_map( $repeat_sanitize_cb, $raw_repeat_value ) + : call_user_func( $repeat_sanitize_cb, $raw_repeat_value ); + + $repeat_fields['fields'][ $formatted_key ] = $sanitized_repeat_value; + $meta_key_value[ $formatted_key ] = $sanitized_repeat_value; } } } diff --git a/wpuf-functions.php b/wpuf-functions.php index fe2f0bc2e..f3941ba19 100644 --- a/wpuf-functions.php +++ b/wpuf-functions.php @@ -1245,7 +1245,7 @@ function wpuf_show_custom_fields( $content ) { $address_html .= '
  • '; if ( 'no' === $hide_label ) { - $address_html .= ' '; + $address_html .= ' '; } $address_html .= ' ' . $value . '
  • '; @@ -1275,7 +1275,7 @@ function wpuf_show_custom_fields( $content ) { $repeat_html = '
  • '; if ( 'no' === $hide_label ) { - $repeat_html .= ''; + $repeat_html .= ''; } $repeat_html .= '
      '; @@ -1294,7 +1294,7 @@ function wpuf_show_custom_fields( $content ) { $repeat_html .= '
    • '; if ( 'no' === $inner_field['hide_field_label'] ) { - $repeat_html .= ' '; + $repeat_html .= ' '; } // Handle different field types @@ -1338,12 +1338,12 @@ function wpuf_show_custom_fields( $content ) { $preview_width = isset( $attr['preview_width'] ) ? $attr['preview_width'] : '123'; $preview_height = isset( $attr['preview_height'] ) ? $attr['preview_height'] : '456'; - $shortcode = '[embed width="' . $preview_width . '" height="' . $preview_height . '"]' . $value . '[/embed]'; + $shortcode = '[embed width="' . $preview_width . '" height="' . $preview_height . '"]' . esc_url_raw( $value ) . '[/embed]'; $preview = '
    • '; if ( 'no' === $hide_label ) { - $preview .= sprintf( '', $attr['label'] ); + $preview .= sprintf( '', esc_html( $attr['label'] ) ); } $preview .= "
      "; @@ -1360,10 +1360,10 @@ function wpuf_show_custom_fields( $content ) { $link = '
    • '; if ( 'no' === $hide_label ) { - $link .= ''; + $link .= ''; } - $link .= sprintf( " %s
    • ", $value, $open_in, $value ); + $link .= sprintf( " %s", esc_url( $value ), esc_attr( $open_in ), esc_html( $value ) ); $html .= $link; break; @@ -1374,7 +1374,7 @@ function wpuf_show_custom_fields( $content ) { $html .= '
    • '; if ( 'no' === $hide_label ) { - $html .= ''; + $html .= ''; } $html .= sprintf( ' %s
    • ', make_clickable( strip_shortcodes( $value ) ) ); @@ -1392,7 +1392,7 @@ function wpuf_show_custom_fields( $content ) { $html .= '
    • '; if ( 'no' === $hide_label ) { - $html .= ''; + $html .= ''; } $html .= sprintf( ' %s
    • ', make_clickable( strip_shortcodes( $value ) ) ); @@ -1413,7 +1413,7 @@ function wpuf_show_custom_fields( $content ) { $html .= '
    • '; if ( 'no' === $hide_label ) { - $html .= ''; + $html .= ''; } $html .= sprintf( ' %s
    • ', make_clickable( strip_shortcodes( $modified_value ) ) ); @@ -1426,7 +1426,7 @@ function wpuf_show_custom_fields( $content ) { $html .= '
    • '; if ( 'no' === $hide_label ) { - $html .= ''; + $html .= ''; } $html .= sprintf( ' %s
    • ', make_clickable( strip_shortcodes( $modified_value ) ) ); @@ -1439,7 +1439,7 @@ function wpuf_show_custom_fields( $content ) { $html .= '
    • '; if ( 'no' === $hide_label ) { - $html .= ''; + $html .= ''; } $html .= sprintf( ' %s
    • ', make_clickable( strip_shortcodes( $new ) ) ); @@ -2367,11 +2367,11 @@ function wpuf_get_completed_transactions( $args = [] ) { $args = wp_parse_args( $args, $defaults ); - if ( ! in_array( $args['orderby'], $orderby ) ) { + if ( ! in_array( $args['orderby'], $orderby, true ) ) { $args['orderby'] = 'id'; } - if ( ! in_array( $args['order'], $order ) ) { + if ( ! in_array( $args['order'], $order, true ) ) { $args['order'] = 'DESC'; } @@ -2379,7 +2379,10 @@ function wpuf_get_completed_transactions( $args = [] ) { return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wpuf_transaction" ); } - $result = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wpuf_transaction ORDER BY `{$args['orderby']}` {$args['order']} LIMIT {$args['offset']}, {$args['number']}", OBJECT ); + $offset = absint( $args['offset'] ); + $number = absint( $args['number'] ); + + $result = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wpuf_transaction ORDER BY `{$args['orderby']}` {$args['order']} LIMIT {$offset}, {$number}", OBJECT ); return $result; } @@ -2407,11 +2410,11 @@ function wpuf_get_pending_transactions( $args = [] ) { $args = wp_parse_args( $args, $defaults ); - if ( ! in_array( $args['orderby'], $orderby ) ) { + if ( ! in_array( $args['orderby'], $orderby, true ) ) { $args['orderby'] = 'id'; } - if ( ! in_array( $args['order'], $order ) ) { + if ( ! in_array( $args['order'], $order, true ) ) { $args['order'] = 'DESC'; } @@ -2504,8 +2507,8 @@ function wpuf_get_all_transactions( $args = [] ) { ); } - $orderby = in_array( $args['orderby'], $orderby_keys ) ? $args['orderby'] : 'id'; - $sorting_order = in_array( $args['order'], $order_keys ) ? $args['order'] : 'DESC'; + $orderby = in_array( $args['orderby'], $orderby_keys, true ) ? $args['orderby'] : 'id'; + $sorting_order = in_array( $args['order'], $order_keys, true ) ? $args['order'] : 'DESC'; $offset = ( int ) sanitize_key( $args['offset'] ); $number = ( int ) sanitize_key( $args['number'] ); @@ -5415,7 +5418,14 @@ function wpuf_guess_username( $email ) { function wpuf_clear_schedule_lock() { check_ajax_referer( 'wpuf_nonce', 'nonce' ); - $post_id = isset( $_POST['post_id'] ) ? intval( wp_unslash( $_POST['post_id'] ) ) : ''; + // Clearing an edit-lock (potentially set by an admin) requires the ability + // to edit others' posts. This prevents any logged-in subscriber from wiping + // lock meta on arbitrary posts. + if ( ! current_user_can( 'edit_others_posts' ) ) { + wp_send_json_error( esc_html__( 'You are not allowed to clear this lock.', 'wp-user-frontend' ), 403 ); + } + + $post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0; if ( ! empty( $post_id ) ) { update_post_meta( $post_id, '_wpuf_lock_user_editing_post_time', '' ); From 7a8cd78fcfe66087096b82c228854455a4877006 Mon Sep 17 00:00:00 2001 From: Ariful Hoque Date: Sun, 5 Jul 2026 12:50:55 +0600 Subject: [PATCH 2/2] style: keep PHPCS-clean for audit fixes (no new violations) Reformat repeat-field sanitizer closures to WPCS style and restore the original single-line // phpcs:ignore on the settings-desc callback, so the security-fix changes introduce zero new PHPCS errors vs develop. Co-Authored-By: Claude Fable 5 --- Lib/WeDevs_Settings_API.php | 4 +--- includes/Traits/FieldableTrait.php | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Lib/WeDevs_Settings_API.php b/Lib/WeDevs_Settings_API.php index 544fe41eb..aae61602c 100644 --- a/Lib/WeDevs_Settings_API.php +++ b/Lib/WeDevs_Settings_API.php @@ -107,9 +107,7 @@ function admin_init() { if ( isset($section['desc']) && !empty($section['desc']) ) { $section['desc'] = '
      ' . $section['desc'] . '
      '; - $callback = function() use ( $section ) { - echo wp_kses_post( $section['desc'] ); - }; + $callback = function () use ( $section ) { echo wp_kses_post( $section['desc'] ); }; // phpcs:ignore } else if ( isset( $section['callback'] ) ) { $callback = $section['callback']; } else { diff --git a/includes/Traits/FieldableTrait.php b/includes/Traits/FieldableTrait.php index fb4822777..8d5266cc7 100644 --- a/includes/Traits/FieldableTrait.php +++ b/includes/Traits/FieldableTrait.php @@ -887,7 +887,12 @@ public static function prepare_meta_fields( $meta_vars ) { if ( in_array( $inner_field['template'], [ 'checkbox_field', 'multiple_select' ] ) ) { // For checkbox and multiselect, keep as array and sanitize each element if ( is_array( $row[ $fname ] ) ) { - $sanitized_row[ $fname ] = array_map( function( $item ) { return strip_shortcodes( sanitize_text_field( $item ) ); }, $row[ $fname ] ); + $sanitized_row[ $fname ] = array_map( + function ( $item ) { + return strip_shortcodes( sanitize_text_field( $item ) ); + }, + $row[ $fname ] + ); } else { $sanitized_row[ $fname ] = strip_shortcodes( sanitize_text_field( $row[ $fname ] ) ); } @@ -906,11 +911,18 @@ public static function prepare_meta_fields( $meta_vars ) { // Fallback to old logic for single-field repeaters or legacy structure. // Sanitize each element at the storage layer (no output escaping so // structured/multi-value data still round-trips). - $meta_key_value[ $value['name'] ] = is_array( $repeater_value ) ? implode( - self::$separator, array_map( function( $item ) { - return strip_shortcodes( sanitize_text_field( $item ) ); - }, $repeater_value ) - ) : ''; + if ( is_array( $repeater_value ) ) { + $sanitized_repeater = array_map( + function ( $item ) { + return strip_shortcodes( sanitize_text_field( $item ) ); + }, + $repeater_value + ); + + $meta_key_value[ $value['name'] ] = implode( self::$separator, $sanitized_repeater ); + } else { + $meta_key_value[ $value['name'] ] = ''; + } } break;