Plugin Directory

Changeset 3457609


Ignore:
Timestamp:
02/10/2026 01:01:39 AM (7 weeks ago)
Author:
lkoudal
Message:

5.265

  • 2026-02-09
  • Tested up to WP 6.9.1
  • FIX: Issues with 2FA for some user.
  • IMPROVED: Vulnerability list updating faster and consume less memory.
Location:
security-ninja
Files:
774 added
2 deleted
4 edited

Legend:

Unmodified
Added
Removed
  • security-ninja/trunk/modules/vulnerabilities/class-wf-sn-vu.php

    r3451093 r3457609  
    66    die( 'Please don\'t open this file directly!' );
    77}
    8 define( 'WF_SN_VU_OPTIONS_NAME', 'wf_sn_vu_settings_group' );
    9 define( 'WF_SN_VU_OPTIONS_KEY', 'wf_sn_vu_settings' );
    10 define( 'WF_SN_VU_OUTDATED', 'wf_sn_vu_outdated' );
    118class Wf_Sn_Vu {
    129    public static $options = null;
     
    456453        // Handle 304 Not Modified.
    457454        if ( 304 === $response_code ) {
    458             // No body processing needed, don't touch file, don't update validators.
     455            // No body processing needed, don't update file content; update validators and touch file so "Last Updated" UI reflects check time.
    459456            if ( $validators ) {
    460457                self::save_file_validators(
     
    475472                    ''
    476473                );
     474            }
     475            // Touch local JSONL file so get_vulnerabilities_last_modified() shows a recent "Last Updated".
     476            require_once ABSPATH . 'wp-admin/includes/file.php';
     477            global $wp_filesystem;
     478            if ( !empty( $wp_filesystem ) || WP_Filesystem() ) {
     479                $upload_dir = wp_upload_dir();
     480                $file_path = $upload_dir['basedir'] . "/security-ninja/vulns/{$type}_vulns.jsonl";
     481                if ( $wp_filesystem->exists( $file_path ) ) {
     482                    $wp_filesystem->touch( $file_path );
     483                }
    477484            }
    478485            $result_info['success'] = true;
     
    689696     * @version v1.0.0  Tuesday, July 25th, 2023.
    690697     * @version v1.0.1  Monday, April 1st, 2024.
    691      * @return  mixed
    692      */
    693     public static function load_vulnerabilities() {
     698     * @param   bool $bypass_cache If true, read from disk and refresh the per-request cache (e.g. after updating files).
     699     * @return  mixed Object with plugins, themes, wordpress arrays, or false on failure.
     700     */
     701    public static function load_vulnerabilities( $bypass_cache = false ) {
     702        static $cached = null;
     703        if ( !$bypass_cache && null !== $cached ) {
     704            return $cached;
     705        }
    694706        require_once ABSPATH . 'wp-admin/includes/file.php';
    695707        // More efficient to require_once at the top if not already included elsewhere
     
    720732            }
    721733        }
    722         return (object) $data;
    723         // Convert back to object if needed for compatibility
     734        $cached = (object) $data;
     735        return $cached;
    724736    }
    725737
     
    769781        }
    770782        return trailingslashit( $upload_dir['basedir'] ) . 'security-ninja/vulns/' . $filenames[$type];
    771     }
    772 
    773     /**
    774      * Stream vulnerability JSONL records line-by-line.
    775      *
    776      * This avoids loading large files into memory. Intended for local files in the uploads directory.
    777      * Yields nothing if the file path is invalid, the file is missing, or the file is not readable.
    778      *
    779      * @since   v5.263
    780      * @param   string $file_path Absolute local file path.
    781      * @return  \Generator Yields each decoded JSON object as an array.
    782      */
    783     private static function stream_jsonl_records( $file_path ) {
    784         if ( empty( $file_path ) || !is_string( $file_path ) ) {
    785             return;
    786         }
    787         if ( !is_file( $file_path ) || !is_readable( $file_path ) ) {
    788             return;
    789         }
    790         $handle = fopen( $file_path, 'rb' );
    791         if ( false === $handle ) {
    792             return;
    793         }
    794         try {
    795             while ( ($line = fgets( $handle )) !== false ) {
    796                 $line = trim( $line );
    797                 if ( '' === $line ) {
    798                     continue;
    799                 }
    800                 $decoded = json_decode( $line, true );
    801                 if ( is_array( $decoded ) ) {
    802                     (yield $decoded);
    803                 }
    804             }
    805         } finally {
    806             if ( is_resource( $handle ) ) {
    807                 fclose( $handle );
    808             }
    809         }
    810     }
    811 
    812     /**
    813      * Count valid JSONL records in a file without loading it into memory.
    814      *
    815      * @since   v5.263
    816      * @param   string $file_path Absolute local file path.
    817      * @return  int Count of valid JSONL records.
    818      */
    819     private static function count_jsonl_records( $file_path ) {
    820         $count = 0;
    821         try {
    822             foreach ( self::stream_jsonl_records( $file_path ) as $record ) {
    823                 ++$count;
    824             }
    825         } catch ( \Throwable $e ) {
    826             return 0;
    827         }
    828         return $count;
    829783    }
    830784
     
    889843        }
    890844        self::ensure_vulns_directory();
    891         $oldcount = false;
    892         $newcount = false;
    893845        $old_data = self::load_vulnerabilities();
    894         $oldcount = 0;
    895         if ( $old_data ) {
    896             $oldcount = self::return_known_vuln_count();
    897         }
     846        $oldcount = ( $old_data ? count( $old_data->plugins ) + count( $old_data->themes ) + count( $old_data->wordpress ) : 0 );
     847        $download_results = array();
    898848        foreach ( self::$api_urls as $type => $url ) {
    899849            // Use conditional GET to download file.
    900850            $result = self::download_vuln_file_with_conditional_get( $type, $url );
    901             // Result is handled within download_vuln_file_with_conditional_get().
    902             // Continue to next file regardless of result.
    903         }
    904         $newcount = self::return_known_vuln_count();
     851            $download_results[$type] = $result;
     852        }
     853        // Log summary when all three types returned 304 (no updates needed).
     854        $all_304 = count( $download_results ) === 3;
     855        foreach ( $download_results as $result ) {
     856            if ( !is_array( $result ) || empty( $result['status_code'] ) || 304 !== (int) $result['status_code'] ) {
     857                $all_304 = false;
     858                break;
     859            }
     860        }
     861        if ( $all_304 && secnin_fs()->can_use_premium_code__premium_only() ) {
     862            wf_sn_el_modules::log_event(
     863                'security_ninja',
     864                'vulnerabilities_update',
     865                __( 'Checked for updates to the vulnerability list; no updates needed.', 'security-ninja' ),
     866                ''
     867            );
     868        }
     869        $new_data = self::load_vulnerabilities( true );
     870        // Bypass cache so we see the newly downloaded files.
     871        $newcount = ( $new_data ? count( $new_data->plugins ) + count( $new_data->themes ) + count( $new_data->wordpress ) : 0 );
     872        if ( $new_data ) {
     873            update_option( 'wf_sn_known_vuln_db_counts', array(
     874                'plugins'   => count( $new_data->plugins ),
     875                'themes'    => count( $new_data->themes ),
     876                'wordpress' => count( $new_data->wordpress ),
     877            ), false );
     878        }
    905879        if ( $oldcount && $newcount ) {
    906880            $diff = $newcount - $oldcount;
     
    10321006        if ( count( array_filter( $x, 'is_array' ) ) > 0 ) {
    10331007            return true;
    1034         }
    1035         return false;
    1036     }
    1037 
    1038     /**
    1039      * Convert an object to an array.
    1040      *
    1041      * @author  Lars Koudal
    1042      * @since   v0.0.1
    1043      * @version v1.0.0  Friday, January 1st, 2021.
    1044      * @param   mixed   $object The object to convert
    1045      * @return  mixed
    1046      */
    1047     public static function object_to_array_map( $object_var ) {
    1048         if ( !is_object( $object_var ) && !is_array( $object_var ) ) {
    1049             return $object_var;
    1050         }
    1051         return array_map( array(__NAMESPACE__ . '\\wf_sn_vu', 'object_to_array'), (array) $object_var );
    1052     }
    1053 
    1054     /**
    1055      * Check if a value exists in the array/object.
    1056      *
    1057      * @author  Lars Koudal
    1058      * @since   v0.0.1
    1059      * @version v1.0.0  Friday, January 1st, 2021.
    1060      * @param   mixed   $needle     The value that you are searching for
    1061      * @param   mixed   $haystack   The array/object to search
    1062      * @param   boolean $strict     Whether to use strict search or not
    1063      * @return  boolean
    1064      */
    1065     public static function search_for_value( $needle, $haystack, $strict = true ) {
    1066         $haystack = self::object_to_array( $haystack );
    1067         if ( is_array( $haystack ) ) {
    1068             if ( self::is_multi_array( $haystack ) ) {
    1069                 // Multidimensional array
    1070                 foreach ( $haystack as $subhaystack ) {
    1071                     if ( self::search_for_value( $needle, $subhaystack, $strict ) ) {
    1072                         return true;
    1073                     }
    1074                 }
    1075             } elseif ( array_keys( $haystack ) !== range( 0, count( $haystack ) - 1 ) ) {
    1076                 // Associative array
    1077                 foreach ( $haystack as $key => $val ) {
    1078                     if ( $needle === $val && !$strict ) {
    1079                         return true;
    1080                     } elseif ( $needle === $val && $strict ) {
    1081                         return true;
    1082                     }
    1083                 }
    1084                 return false;
    1085             } elseif ( $needle === $haystack && !$strict ) {
    1086                 // Normal array
    1087                 return true;
    1088             } elseif ( $needle === $haystack && $strict ) {
    1089                 return true;
    1090             }
    10911008        }
    10921009        return false;
     
    12781195        $lookup_id = 0;
    12791196        try {
    1280             $wp_file_path = self::get_vuln_jsonl_file_path( 'wordpress' );
    1281             if ( empty( $wp_file_path ) || !is_file( $wp_file_path ) || !is_readable( $wp_file_path ) ) {
     1197            $data = self::load_vulnerabilities();
     1198            if ( !$data || !isset( $data->wordpress ) ) {
    12821199                self::ensure_vulns_directory();
    12831200                wp_schedule_single_event( time(), 'secnin_update_vuln_list' );
    12841201            } else {
    1285                 foreach ( self::stream_jsonl_records( $wp_file_path ) as $wpvuln ) {
     1202                foreach ( $data->wordpress as $wpvuln ) {
    12861203                    if ( empty( $wpvuln['versionEndExcluding'] ) || empty( $wpvuln['CVE_ID'] ) ) {
    12871204                        continue;
     
    13881305    }
    13891306
    1390     /**
    1391      * Returns the number of known vulnerabilities
    1392      *
    1393      * @author  Lars Koudal
    1394      * @author  Unknown
    1395      * @since   v0.0.1
    1396      * @version v1.0.0  Tuesday, July 6th, 2021.
    1397      * @version v1.0.1  Monday, April 1st, 2024.
    1398      * @return  mixed
    1399      */
    1400     public static function return_known_vuln_count() {
    1401         $plugin_file = self::get_vuln_jsonl_file_path( 'plugins' );
    1402         $themes_file = self::get_vuln_jsonl_file_path( 'themes' );
    1403         $wordpress_file = self::get_vuln_jsonl_file_path( 'wordpress' );
    1404         $plugin_vulns_count = ( $plugin_file ? self::count_jsonl_records( $plugin_file ) : 0 );
    1405         $theme_vulns_count = ( $themes_file ? self::count_jsonl_records( $themes_file ) : 0 );
    1406         $wp_vulns_count = ( $wordpress_file ? self::count_jsonl_records( $wordpress_file ) : 0 );
    1407         $total = $plugin_vulns_count + $theme_vulns_count + $wp_vulns_count;
    1408         if ( 0 === $total ) {
    1409             $any_missing = false;
    1410             foreach ( array($plugin_file, $themes_file, $wordpress_file) as $path ) {
    1411                 if ( !empty( $path ) && (!is_file( $path ) || !is_readable( $path )) ) {
    1412                     $any_missing = true;
    1413                     break;
    1414                 }
    1415             }
    1416             if ( $any_missing ) {
    1417                 self::ensure_vulns_directory();
    1418                 wp_schedule_single_event( time(), 'secnin_update_vuln_list' );
    1419             }
    1420         }
    1421         return $total;
    1422     }
    1423 
    14241307    public static function get_vuln_details() {
    1425         $plugin_file = self::get_vuln_jsonl_file_path( 'plugins' );
    1426         $themes_file = self::get_vuln_jsonl_file_path( 'themes' );
    1427         $wordpress_file = self::get_vuln_jsonl_file_path( 'wordpress' );
     1308        $counts = get_option( 'wf_sn_known_vuln_db_counts', false );
     1309        if ( is_array( $counts ) && isset( $counts['plugins'], $counts['themes'], $counts['wordpress'] ) ) {
     1310            return array(
     1311                'plugins'   => (int) $counts['plugins'],
     1312                'themes'    => (int) $counts['themes'],
     1313                'wordpress' => (int) $counts['wordpress'],
     1314            );
     1315        }
     1316        // Option not set yet. Do not load files; wait for update_vuln_list() to populate.
    14281317        return array(
    1429             'plugins'   => ( $plugin_file ? self::count_jsonl_records( $plugin_file ) : 0 ),
    1430             'themes'    => ( $themes_file ? self::count_jsonl_records( $themes_file ) : 0 ),
    1431             'wordpress' => ( $wordpress_file ? self::count_jsonl_records( $wordpress_file ) : 0 ),
     1318            'plugins'   => 0,
     1319            'themes'    => 0,
     1320            'wordpress' => 0,
    14321321        );
    1433     }
    1434 
    1435     /**
    1436      * Helper method to count vulnerabilities in a more abstract way
    1437      *
    1438      * @author  Unknown
    1439      * @since   v0.0.1
    1440      * @version v1.0.0  Monday, April 1st, 2024.
    1441      * @param   mixed   $vuln_type
    1442      * @return  mixed
    1443      */
    1444     private static function count_vulns( $vuln_type ) {
    1445         return ( isset( $vuln_type ) ? count( $vuln_type ) : 0 );
    14461322    }
    14471323
     
    14971373                self::update_vuln_list();
    14981374            }
    1499             $plugin_file = self::get_vuln_jsonl_file_path( 'plugins' );
    1500             $themes_file = self::get_vuln_jsonl_file_path( 'themes' );
    1501             $wordpress_file = self::get_vuln_jsonl_file_path( 'wordpress' );
    1502             $plugin_vulns_count = ( $plugin_file ? self::count_jsonl_records( $plugin_file ) : 0 );
    1503             $theme_vulns_count = ( $themes_file ? self::count_jsonl_records( $themes_file ) : 0 );
    1504             $wp_vulns_count = ( $wordpress_file ? self::count_jsonl_records( $wordpress_file ) : 0 );
     1375            $vuln_details = self::get_vuln_details();
     1376            $plugin_vulns_count = $vuln_details['plugins'];
     1377            $theme_vulns_count = $vuln_details['themes'];
     1378            $wp_vulns_count = $vuln_details['wordpress'];
    15051379            $total_vulnerabilities = $plugin_vulns_count + $wp_vulns_count + $theme_vulns_count;
    15061380        }
     
    17061580                        $current_time = time();
    17071581                        $time_diff = human_time_diff( $timestamp, $current_time );
    1708                         // Check if file is outdated (older than 2 days)
    1709                         $two_days_ago = $current_time - 2 * 24 * 60 * 60;
    1710                         $is_outdated = $timestamp < $two_days_ago;
     1582                        // Check if file is outdated (older than 1 day)
     1583                        $one_day_ago = $current_time - 1 * 24 * 60 * 60;
     1584                        $is_outdated = $timestamp < $one_day_ago;
    17111585                        // If the timestamp is in the future or very recent, show appropriate message
    17121586                        if ( $timestamp > $current_time ) {
     
    19761850            $installed_set = array_fill_keys( array_keys( $plugin_slug_map ), true );
    19771851        }
    1978         foreach ( self::stream_jsonl_records( $file_path ) as $decoded_line ) {
     1852        $data = self::load_vulnerabilities();
     1853        $plugin_records = ( $data && isset( $data->plugins ) ? $data->plugins : array() );
     1854        foreach ( $plugin_records as $decoded_line ) {
    19791855            ++$scan_stats['lines_processed'];
    19801856            if ( !isset( $decoded_line['slug'] ) ) {
     
    20391915            'stats'           => $scan_stats,
    20401916        );
    2041     }
    2042 
    2043     /**
    2044      * Convert memory limit string to bytes
    2045      *
    2046      * @param   string  $val Memory limit string (e.g., '256M', '1G')
    2047      * @return  int     Memory limit in bytes
    2048      */
    2049     private static function return_bytes( $val ) {
    2050         $val = trim( $val );
    2051         $last = strtolower( $val[strlen( $val ) - 1] );
    2052         $val = (int) $val;
    2053         switch ( $last ) {
    2054             case 'g':
    2055                 $val *= 1024;
    2056             case 'm':
    2057                 $val *= 1024;
    2058             case 'k':
    2059                 $val *= 1024;
    2060         }
    2061         return $val;
    20621917    }
    20631918
     
    23772232            $ignored_set = array_fill_keys( $ignored_slugs, true );
    23782233        }
    2379         foreach ( self::stream_jsonl_records( $file_path ) as $decoded_line ) {
     2234        $data = self::load_vulnerabilities();
     2235        $theme_records = ( $data && isset( $data->themes ) ? $data->themes : array() );
     2236        foreach ( $theme_records as $decoded_line ) {
    23802237            ++$scan_stats['lines_processed'];
    23812238            if ( !isset( $decoded_line['slug'] ) ) {
     
    27062563        $error_count = 0;
    27072564        $errors = array();
     2565        $results = array();
    27082566        // Download all vulnerability files using conditional GET.
    27092567        foreach ( self::$api_urls as $file_type => $api_url ) {
    27102568            $result = self::download_vuln_file_with_conditional_get( $file_type, $api_url );
     2569            $results[$file_type] = $result;
    27112570            if ( $result && isset( $result['success'] ) && $result['success'] ) {
    27122571                ++$success_count;
  • security-ninja/trunk/readme.txt

    r3451093 r3457609  
    66License URI: https://www.gnu.org/licenses/gpl-3.0.html
    77Requires at least: 4.7
    8 Tested up to: 6.9
    9 Stable tag: 5.264
     8Tested up to: 6.9.1
     9Stable tag: 5.265
    1010Requires PHP: 7.4
    1111
    12 WordPress security plugin with a **basic firewall/WAF (free)**, vulnerability scanning, and core integrity checks — with optional Pro malware scanning and advanced WAF controls.
     12WordPress security plugin with free basic firewall/WAF, vulnerability scanning, and 50+ core integrity checks.
    1313
    1414== Description ==
     
    333333== Changelog ==
    334334
     335= 5.265 =
     336* 2026-02-09
     337* Tested up to WP 6.9.1
     338* FIX: Issues with 2FA for some user.
     339* IMPROVED: Vulnerability list updating faster and consume less memory.
     340
    335341= 5.264 =
    336342* 2026-01-31
  • security-ninja/trunk/security-ninja.php

    r3451093 r3457609  
    66Description: Check your site for security vulnerabilities and get precise suggestions for corrective actions on passwords, user accounts, file permissions, database security, version hiding, plugins, themes, security headers and other security aspects.
    77Author: WP Security Ninja
    8 Version: 5.264
     8Version: 5.265
    99Author URI: https://wpsecurityninja.com/
    1010License: GPLv3
  • security-ninja/trunk/vendor/composer/installed.php

    r3451093 r3457609  
    44        'pretty_version' => 'dev-master',
    55        'version' => 'dev-master',
    6         'reference' => '3801a96cc9a8761ed43c2113f7e43fba98c9753f',
     6        'reference' => '1dce507ca2f2360d8ac6dd12cef0d4573c3c471d',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
     
    149149            'pretty_version' => 'dev-master',
    150150            'version' => 'dev-master',
    151             'reference' => '3801a96cc9a8761ed43c2113f7e43fba98c9753f',
     151            'reference' => '1dce507ca2f2360d8ac6dd12cef0d4573c3c471d',
    152152            'type' => 'library',
    153153            'install_path' => __DIR__ . '/../../',
Note: See TracChangeset for help on using the changeset viewer.