| @@ -784,9 +784,14 @@ | ||
| 784 | 784 | $defined = explode( ',', trim( (string) $args ) ); |
| 785 | 785 | } elseif( is_integer( $args ) || is_float( $args ) ) { |
| 786 | 786 | $defined[0] = $args; |
| 787 | 787 | } elseif( is_serialized( $args ) ) { |
| 788 | - $defined = unserialize( $args ); | |
| 788 | + // SECURITY: refuse to instantiate any class — only scalars and arrays | |
| 789 | + // of scalars are recovered. Blocks PHP Object Injection via widget | |
| 790 | + // AJAX (forumids etc.) and any other caller that may receive | |
| 791 | + // attacker-controlled strings. | |
| 792 | + $defined = unserialize( $args, [ 'allowed_classes' => false ] ); | |
| 793 | + if( ! is_array( $defined ) ) $defined = (array) $defined; | |
| 789 | 794 | } elseif( strpos( (string) $args, '=' ) !== false ) { |
| 790 | 795 | parse_str( $args, $defined ); |
| 791 | 796 | } else { |
| 792 | 797 | $defined = (array) $args; |
| @@ -810,9 +815,13 @@ | ||
| 810 | 815 | if( ! function_exists( 'is_serialized' ) ) { |
| 811 | 816 | function is_serialized( $value ) { |
| 812 | 817 | if( $value == '' ) return false; |
| 813 | 818 | $value = trim( (string) $value ); |
| 814 | - $chsd = @unserialize( $value ); | |
| 819 | + // SECURITY: the very act of detecting a serialized payload must not | |
| 820 | + // instantiate classes — @unserialize() with the default options runs | |
| 821 | + // __wakeup() / __destruct() for any embedded object even when the | |
| 822 | + // caller only wanted to test the format. Limit to scalars/arrays. | |
| 823 | + $chsd = @unserialize( $value, [ 'allowed_classes' => false ] ); | |
| 815 | 824 | if( $chsd !== false || $value === 'b:0;' ) { |
| 816 | 825 | return true; |
| 817 | 826 | } else { |
| 818 | 827 | return false; |
| @@ -2815,8 +2824,89 @@ | ||
| 2815 | 2824 | return true; |
| 2816 | 2825 | } else { |
| 2817 | 2826 | return false; |
| 2818 | 2827 | } |
| 2828 | +} | |
| 2829 | + | |
| 2830 | +/** | |
| 2831 | + * Add a post to the guest's signed ownership cookie. | |
| 2832 | + * Called when a guest creates a new post or topic. | |
| 2833 | + * | |
| 2834 | + * @param int $postid The post ID to add to ownership | |
| 2835 | + */ | |
| 2836 | +function wpforo_add_guest_ownership( $postid ) { | |
| 2837 | + // Respect the forum cookie policy, like every other wpForo cookie writer. | |
| 2838 | + if( ! wpforo_setting( 'legal', 'cookies' ) ) return; | |
| 2839 | + | |
| 2840 | + $postid = (int) $postid; | |
| 2841 | + if( ! $postid ) return; | |
| 2842 | + | |
| 2843 | + $owned = wpforo_get_guest_owned_posts(); | |
| 2844 | + $owned[] = $postid; | |
| 2845 | + | |
| 2846 | + // Keep the newest N ids only, so the cookie can't grow past browser limits. | |
| 2847 | + $max = (int) apply_filters( 'wpforo_guest_ownership_max_posts', 100 ); | |
| 2848 | + $owned = array_values( array_unique( $owned ) ); | |
| 2849 | + if( $max > 0 && count( $owned ) > $max ) $owned = array_slice( $owned, - $max ); | |
| 2850 | + | |
| 2851 | + $data = wp_json_encode( $owned ); | |
| 2852 | + if( ! is_string( $data ) ) return; | |
| 2853 | + | |
| 2854 | + $signature = hash_hmac( 'sha256', $data, wp_salt( 'auth' ) ); | |
| 2855 | + $cookie_value = base64_encode( $data ) . '.' . $signature; | |
| 2856 | + | |
| 2857 | + $expire = time() + ( 30 * DAY_IN_SECONDS ); | |
| 2858 | + $secure = is_ssl() && 'https' === parse_url( get_option( 'home' ), PHP_URL_SCHEME ); | |
| 2859 | + $path = ( COOKIEPATH != SITECOOKIEPATH ) ? SITECOOKIEPATH : COOKIEPATH; | |
| 2860 | + | |
| 2861 | + // Suppressed: output may already have started on some themes/hooks, and a | |
| 2862 | + // failed cookie must degrade to "cannot edit", never to a PHP warning. | |
| 2863 | + @setcookie( 'wpforo_guest_ownership', $cookie_value, $expire, $path, COOKIE_DOMAIN, $secure, true ); | |
| 2864 | + | |
| 2865 | + // Make it readable within the same request. | |
| 2866 | + $_COOKIE['wpforo_guest_ownership'] = $cookie_value; | |
| 2867 | +} | |
| 2868 | + | |
| 2869 | +/** | |
| 2870 | + * Get list of post IDs this guest owns (verified via signature). | |
| 2871 | + * Returns empty array if cookie is missing, corrupted, or signature invalid. | |
| 2872 | + * | |
| 2873 | + * @return array List of post IDs | |
| 2874 | + */ | |
| 2875 | +function wpforo_get_guest_owned_posts() { | |
| 2876 | + $cookie = isset( $_COOKIE['wpforo_guest_ownership'] ) ? $_COOKIE['wpforo_guest_ownership'] : ''; | |
| 2877 | + | |
| 2878 | + // The cookie is fully attacker-controlled: it can arrive as an array | |
| 2879 | + // (wpforo_guest_ownership[]=x), which would fatal on strpos() in PHP 8. | |
| 2880 | + if( ! is_string( $cookie ) || $cookie === '' ) return []; | |
| 2881 | + | |
| 2882 | + $parts = explode( '.', $cookie, 2 ); | |
| 2883 | + if( count( $parts ) !== 2 ) return []; | |
| 2884 | + | |
| 2885 | + list( $data_b64, $signature ) = $parts; | |
| 2886 | + $data = base64_decode( $data_b64, true ); | |
| 2887 | + if( ! is_string( $data ) || $data === '' ) return []; | |
| 2888 | + | |
| 2889 | + // Verify HMAC signature - prevents forgery | |
| 2890 | + $expected = hash_hmac( 'sha256', $data, wp_salt( 'auth' ) ); | |
| 2891 | + if( ! hash_equals( $expected, $signature ) ) return []; | |
| 2892 | + | |
| 2893 | + $owned = json_decode( $data, true ); | |
| 2894 | + if( ! is_array( $owned ) ) return []; | |
| 2895 | + | |
| 2896 | + return array_values( array_filter( array_map( 'intval', $owned ) ) ); | |
| 2897 | +} | |
| 2898 | + | |
| 2899 | +/** | |
| 2900 | + * Check if the current guest owns a specific post. | |
| 2901 | + * Uses cryptographically signed cookie - cannot be forged. | |
| 2902 | + * | |
| 2903 | + * @param int $postid The post ID to check | |
| 2904 | + * @return bool True if guest owns this post | |
| 2905 | + */ | |
| 2906 | +function wpforo_guest_owns_post( $postid ) { | |
| 2907 | + $owned = wpforo_get_guest_owned_posts(); | |
| 2908 | + return in_array( (int) $postid, $owned, true ); | |
| 2819 | 2909 | } |
| 2820 | 2910 | |
| 2821 | 2911 | function wpforo_extra_html_parser( $extra_html = '', $allowed_html = [] ) { |
| 2822 | 2912 | if( $extra_html ) { |