Plugin Directory

Changeset 3316692


Ignore:
Timestamp:
06/24/2025 06:01:25 AM (9 months ago)
Author:
teamwpminiorange
Message:

Prevent files and folders access plugin release v2.6.1

Location:
prevent-file-access
Files:
58 added
3 edited

Legend:

Unmodified
Added
Removed
  • prevent-file-access/trunk/README.txt

    r3297575 r3316692  
    66Tested up to: 6.8
    77Requires PHP: 5.6
    8 Stable tag: 2.6.0
     8Stable tag: 2.6.1
    99License: Expat
    1010License URI: https://plugins.miniorange.com/mit-license
     
    143143
    144144== Changelog ==
     145= 2.6.1 =
     146* Security fixes
     147
    145148= 2.6.0 =
    146149* Added Compatibility with WordPress 6.8
     
    226229
    227230== Upgrade Notice ==
     231= 2.6.1 =
     232* Security fixes
     233
    228234= 2.6.0 =
    229235* Added Compatibility with WordPress 6.8
  • prevent-file-access/trunk/admin/class-media-restriction-admin.php

    r2957943 r3316692  
    6464
    6565    /**
     66     * Validate and sanitize file path to prevent path traversal attacks
     67     *
     68     * @param string $path The path to validate.
     69     * @return string|false The validated path or false if invalid
     70     */
     71    private function mo_media_restriction_validate_path( $path ) {
     72        $path = str_replace( array( "\0", "\x00" ), '', $path );
     73        $path = str_replace( '\\', '/', $path );
     74
     75        $previous_path = '';
     76        while ( $path !== $previous_path ) {
     77            $previous_path = $path;
     78            $path          = urldecode( $path );
     79        }
     80
     81        $path = str_replace( '\\', '/', $path );
     82
     83        $dangerous_patterns = array(
     84            '../',
     85            '..\\',
     86            '%2e%2e%2f',
     87            '%2e%2e%5c',
     88            '%252e%252e%252f',
     89            '%252e%252e%255c',
     90            '0x2e0x2e0x2f',
     91            '0x2e0x2e0x5c',
     92            '%c0%ae%c0%ae%c0%af',
     93            '%c1%9c',
     94            '..%2f',
     95            '..%5c',
     96            '%2e.',
     97            '.%2e',
     98        );
     99
     100        foreach ( $dangerous_patterns as $pattern ) {
     101            if ( stripos( $path, $pattern ) !== false ) {
     102                $this->mo_media_restriction_log_security_event( 'Dangerous path pattern detected', $path );
     103                return false;
     104            }
     105        }
     106
     107        if ( substr( $path, 0, 1 ) === '/' || ( strlen( $path ) > 1 && substr( $path, 1, 1 ) === ':' ) ) {
     108            return false;
     109        }
     110
     111        $full_path = ABSPATH . DIRECTORY_SEPARATOR . ltrim( $path, '/' );
     112
     113        $real_path = realpath( $full_path );
     114
     115        if ( false === $real_path ) {
     116            return false;
     117        }
     118
     119        $wp_root = realpath( ABSPATH );
     120        if ( strpos( $real_path, $wp_root ) !== 0 ) {
     121            return false;
     122        }
     123
     124        $allowed_dirs = $this->mo_media_restriction_get_allowed_directories();
     125        $is_allowed   = false;
     126
     127        foreach ( $allowed_dirs as $allowed_dir ) {
     128            $allowed_real_path = realpath( $allowed_dir );
     129            if ( false !== $allowed_real_path && strpos( $real_path, $allowed_real_path ) === 0 ) {
     130                $is_allowed = true;
     131                break;
     132            }
     133        }
     134
     135        if ( ! $is_allowed ) {
     136            $this->mo_media_restriction_log_security_event( 'Access to disallowed directory attempted', $path );
     137            return false;
     138        }
     139
     140        return $real_path;
     141    }
     142
     143    /**
     144     * Get list of allowed directories for file access
     145     *
     146     * @return array Array of allowed directory paths
     147     */
     148    private function mo_media_restriction_get_allowed_directories() {
     149        $uploads_dir = wp_upload_dir();
     150
     151        $allowed_dirs = array(
     152            $uploads_dir['basedir'],
     153        );
     154
     155        $protected_dir = $uploads_dir['basedir'] . DIRECTORY_SEPARATOR . 'protectedfiles';
     156        if ( file_exists( $protected_dir ) ) {
     157            $allowed_dirs[] = $protected_dir;
     158        }
     159
     160        $custom_dirs = get_option( 'mo_media_restriction_allowed_dirs', array() );
     161        if ( is_array( $custom_dirs ) ) {
     162            $allowed_dirs = array_merge( $allowed_dirs, $custom_dirs );
     163        }
     164
     165        $allowed_dirs = apply_filters( 'mo_media_restriction_allowed_directories', $allowed_dirs );
     166
     167        return array_unique( $allowed_dirs );
     168    }
     169
     170    /**
    66171     * Show restricted folder path
    67172     *
     
    70175     */
    71176    public function mo_media_show_file_or_folder( $redirect_url ) {
    72         $file_path = ABSPATH . DIRECTORY_SEPARATOR . $redirect_url;
    73         $file_url  = site_url() . '/' . $redirect_url;
     177        $validated_path = $this->mo_media_restriction_validate_path( $redirect_url );
     178
     179        if ( false === $validated_path ) {
     180            wp_die( 'WPMR001: Invalid file path', 'Access Denied', array( 'response' => 403 ) );
     181        }
     182
     183        $file_path     = $validated_path;
     184        $relative_path = str_replace( ABSPATH, '', $validated_path );
     185        $file_url      = site_url() . '/' . ltrim( $relative_path, '/' );
     186
    74187        if ( file_exists( $file_path ) ) {
    75188            if ( is_dir( $file_path ) ) {
    76189                $dh = opendir( $file_path );
    77190                if ( $dh ) {
    78                     // reading the contents of the directory.
     191
    79192                    $file = readdir( $dh );
    80193                    while ( false !== $file ) {
    81194                        if ( '..' !== $file && '.' !== $file ) {
    82                             echo "<a href='" . esc_attr( $file_url ) . '/' . esc_attr( $file ) . "'>" . esc_attr( $file ) . '</a><br>';
     195                            $file_name = sanitize_file_name( $file );
     196                            if ( $file_name === $file ) {
     197                                $child_relative_path = ltrim( str_replace( ABSPATH, '', $validated_path ), '/' );
     198                                $child_url           = site_url() . '/' . $child_relative_path . '/' . $file_name;
     199                                echo "<a href='" . esc_url( $child_url ) . "'>" . esc_html( $file ) . '</a><br>';
     200                            }
    83201                        }
    84202                    }
     
    87205                exit;
    88206            } else {
     207                $allowed_extensions = array( 'jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt' );
     208                $file_extension     = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
     209
     210                if ( ! in_array( $file_extension, $allowed_extensions, true ) ) {
     211                    wp_die( 'WPMR002: File type not allowed', 'Access Denied', array( 'response' => 403 ) );
     212                }
     213
    89214                header( 'content-type: ' . mime_content_type( $file_path ) );
    90                 echo esc_attr( wp_remote_get( $file_path ) );
     215                readfile( $file_path );
    91216                exit;
    92217            }
    93218        } else {
    94             wp_die( 'No such file exist' );
     219            wp_die( 'WPMR003: No such file exist' );
    95220        }
    96221    }
     
    102227     */
    103228    public function mo_media_restriction_validate() {
    104         if ( isset( $_GET['mo_media_restrict_request'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['mo_media_restrict_request'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Ignoring nonce verification because we are not fetching data on form submission.
     229        if ( isset( $_GET['mo_media_restrict_request'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['mo_media_restrict_request'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are implementing custom nonce verification below.
    105230            if ( is_user_logged_in() === false ) {
    106231                $restrict_option = get_option( 'mo_mr_redirect_to' );
     
    114239                exit;
    115240            } else {
    116                 $redirect_url = isset( $_GET['redirect_to'] ) ? sanitize_text_field( wp_unslash( $_GET['redirect_to'] ) ) : site_url(); //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Ignoring nonce verification because we are fetching data from URL and not on form submission.
     241                $redirect_url = isset( $_GET['redirect_to'] ) ? sanitize_text_field( wp_unslash( $_GET['redirect_to'] ) ) : site_url();
     242
     243                if ( empty( $redirect_url ) || strlen( $redirect_url ) > 255 ) {
     244                    wp_die( 'WPMR005: Invalid file path', 'Access Denied', array( 'response' => 403 ) );
     245                }
     246
     247                $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
     248                if ( ! wp_verify_nonce( $nonce, 'mo_media_restriction_access' ) ) {
     249                    $this->mo_media_restriction_log_security_event( 'Invalid nonce for file access', $redirect_url );
     250                    wp_die( 'WPMR006: Invalid file path', 'Access Denied', array( 'response' => 403 ) );
     251                }
     252
     253                if ( strpos( $redirect_url, '..' ) !== false || strpos( $redirect_url, '/' ) === 0 ) {
     254                    $this->mo_media_restriction_log_security_event( 'Path traversal attempt detected', $redirect_url );
     255                    wp_die( 'WPMR007: Invalid file path', 'Access Denied', array( 'response' => 403 ) );
     256                }
     257
     258                if ( ! $this->mo_media_restriction_check_rate_limit() ) {
     259                    wp_die( 'WPMR008: Too many requests. Please try again later.', 'Rate Limit Exceeded', array( 'response' => 429 ) );
     260                }
     261
    117262                $this->mo_media_show_file_or_folder( $redirect_url );
    118263            }
    119264        }
     265    }
     266
     267    /**
     268     * Log security events for monitoring
     269     *
     270     * @param string $event_type Type of security event.
     271     * @param string $details Additional details.
     272     * @return void
     273     */
     274    private function mo_media_restriction_log_security_event( $event_type, $details ) {
     275        $user      = wp_get_current_user();
     276        $log_entry = array(
     277            'timestamp'  => current_time( 'mysql' ),
     278            'user_id'    => $user->ID,
     279            'user_login' => $user->user_login,
     280            'ip_address' => isset( $_SERVER['REMOTE_ADDR'] )
     281            ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) )
     282            : 'unknown',
     283
     284            'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] )
     285            ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
     286            : 'unknown',
     287            'event_type' => $event_type,
     288            'details'    => $details,
     289        );
     290
     291        $existing_logs   = get_option( 'mo_media_restriction_security_logs', array() );
     292        $existing_logs[] = $log_entry;
     293
     294        if ( count( $existing_logs ) > 100 ) {
     295            $existing_logs = array_slice( $existing_logs, -100 );
     296        }
     297
     298        update_option( 'mo_media_restriction_security_logs', $existing_logs );
     299
     300    }
     301
     302    /**
     303     * Simple rate limiting to prevent abuse
     304     *
     305     * @return bool True if request is allowed, false if rate limited
     306     */
     307    private function mo_media_restriction_check_rate_limit() {
     308        $user_id        = get_current_user_id();
     309        $rate_limit_key = "mo_media_restriction_rate_limit_{$user_id}";
     310        $current_time   = time();
     311        $window_size    = 60;
     312        $max_requests   = 30;
     313
     314        $user_requests = get_transient( $rate_limit_key );
     315
     316        if ( false === $user_requests ) {
     317            set_transient( $rate_limit_key, array( $current_time ), $window_size );
     318            return true;
     319        }
     320
     321        $user_requests = array_filter(
     322            $user_requests,
     323            function( $timestamp ) use ( $current_time, $window_size ) {
     324                return ( $current_time - $timestamp ) < $window_size;
     325            }
     326        );
     327
     328        if ( count( $user_requests ) >= $max_requests ) {
     329            return false;
     330        }
     331
     332        $user_requests[] = $current_time;
     333        set_transient( $rate_limit_key, $user_requests, $window_size );
     334
     335        return true;
    120336    }
    121337
     
    671887    }
    672888
     889    /**
     890     * Generate secure URL for file access with proper nonce
     891     *
     892     * @param string $file_path The file path to generate URL for.
     893     * @return string The secure URL with nonce
     894     */
     895    public function mo_media_restriction_generate_secure_url( $file_path ) {
     896        $file_path = sanitize_text_field( $file_path );
     897
     898        $nonce = wp_create_nonce( 'mo_media_restriction_access' );
     899
     900        $secure_url = add_query_arg(
     901            array(
     902                'mo_media_restrict_request' => '1',
     903                'redirect_to'               => $file_path,
     904                '_wpnonce'                  => $nonce,
     905            ),
     906            home_url()
     907        );
     908
     909        return $secure_url;
     910    }
     911
    673912}
  • prevent-file-access/trunk/media-restriction.php

    r3297575 r3316692  
    44 * Plugin URI: http://miniorange.com
    55 * Description: Allows to protect your files and folders (wp-content, uploads, images, pdf, documents) from public access, Role base folder access, User base folder access, giving access to only logged in users.
    6  * Version: 2.6.0
     6 * Version: 2.6.1
    77 * Author: miniOrange
    88 * Author URI: http://miniorange.com
     
    2323 * Rename this for your plugin and update it as you release new versions.
    2424 */
    25 define( 'MO_MEDIA_RESTRICTION_PLUGIN_NAME_VERSION', '2.6.0' );
     25define( 'MO_MEDIA_RESTRICTION_PLUGIN_NAME_VERSION', '2.6.1' );
    2626/**
    2727 * The code that runs during plugin activation.
Note: See TracChangeset for help on using the changeset viewer.