Plugin Directory

Changeset 3488388


Ignore:
Timestamp:
03/22/2026 07:54:44 PM (7 days ago)
Author:
freelancebo
Message:

v2.4.0: Security audit fixes - SSL verification configurable, auto-patch requires approval, HMAC anti-replay, REST rate limiting, firewall regex validation, input sanitization, trusted proxy validation

Location:
freelancebo-sentra-control/trunk
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • freelancebo-sentra-control/trunk/admin/views/settings.php

    r3485719 r3488388  
    6262                </td>
    6363            </tr>
     64            <tr>
     65                <th scope="row"><label for="sentra_auto_patch_enabled"><?php echo esc_html__( 'Auto-Patching', 'freelancebo-sentra-control' ); ?></label></th>
     66                <td>
     67                    <label>
     68                        <input type="checkbox" id="sentra_auto_patch_enabled" name="sentra_auto_patch_enabled" value="1"
     69                               <?php checked(get_option('sentra_auto_patch_enabled', '0'), '1'); ?> />
     70                        <?php echo esc_html__( 'Enable automatic patching from server (without manual approval)', 'freelancebo-sentra-control' ); ?>
     71                    </label>
     72                    <p class="description">
     73                        <?php echo esc_html__( 'When disabled (default), patches pushed from the server require manual approval in the Auto-Patch page.', 'freelancebo-sentra-control' ); ?>
     74                    </p>
     75                </td>
     76            </tr>
     77            <tr>
     78                <th scope="row"><label for="sentra_ssl_verify"><?php echo esc_html__( 'SSL Verification', 'freelancebo-sentra-control' ); ?></label></th>
     79                <td>
     80                    <label>
     81                        <input type="checkbox" id="sentra_ssl_verify" name="sentra_ssl_verify" value="1"
     82                               <?php checked(get_option('sentra_ssl_verify', '1'), '1'); ?> />
     83                        <?php echo esc_html__( 'Verify SSL certificates when connecting to Sentra server', 'freelancebo-sentra-control' ); ?>
     84                    </label>
     85                    <p class="description" style="color: #b32d2e;">
     86                        <?php echo esc_html__( 'Warning: Disabling SSL verification is insecure and should only be used for testing with self-signed certificates.', 'freelancebo-sentra-control' ); ?>
     87                    </p>
     88                </td>
     89            </tr>
    6490        </table>
    6591
  • freelancebo-sentra-control/trunk/freelancebo-sentra-control.php

    r3488339 r3488388  
    44 * Plugin URI: https://freelancebo.it
    55 * Description: WordPress security agent - connects to FreelanceBo Sentra Control central console for WAF, malware scanning, brute force protection, and file integrity monitoring.
    6  * Version: 2.3.1
     6 * Version: 2.4.0
    77 * Author: Freelancebo
    88 * License: GPL-2.0-or-later
     
    1212if (!defined('ABSPATH')) exit;
    1313
    14 define("SENTRA_VERSION", "2.3.1");
     14define("SENTRA_VERSION", "2.4.0");
    1515define('SENTRA_PLUGIN_DIR', plugin_dir_path(__FILE__));
    1616define('SENTRA_PLUGIN_URL', plugin_dir_url(__FILE__));
     
    157157        ]);
    158158
     159        register_setting('sentra_settings', 'sentra_auto_patch_enabled', ['sanitize_callback' => 'sanitize_text_field', 'default' => '0']);
     160        register_setting('sentra_settings', 'sentra_ssl_verify', ['sanitize_callback' => 'sanitize_text_field', 'default' => '1']);
     161
    159162        // AJAX test connection
    160163        add_action('wp_ajax_sentra_test_connection', [$this, 'ajax_test_connection']);
     
    202205
    203206    /**
    204      * Allow self-signed SSL for the Sentra server URL.
     207     * Allow self-signed SSL for the Sentra server URL (only when SSL verify is disabled).
    205208     */
    206209    public function allow_sentra_ssl($args, $url) {
    207210        $server = get_option('sentra_server_url', '');
    208211        if ($server && strpos($url, rtrim($server, '/')) === 0) {
    209             $args['sslverify'] = false;
    210             $args['reject_unsafe_urls'] = false;
     212            if (get_option('sentra_ssl_verify', '1') !== '1') {
     213                $args['sslverify'] = false;
     214                $args['reject_unsafe_urls'] = false;
     215            }
    211216        }
    212217        return $args;
     
    344349            'target'         => sanitize_text_field($patch_data['target'] ?? ''),
    345350            'severity'       => sanitize_text_field($patch_data['severity'] ?? 'medium'),
    346             'source_finding' => $patch_data['source_finding'] ?? [],
     351            'source_finding' => $this->sanitize_recursive($patch_data['source_finding'] ?? []),
    347352            'patch_payload'  => $patch_data['patch_payload'] ?? [],
    348353        ];
     
    478483
    479484        if (isset($_POST['body'])) {
    480             $raw_body = json_decode(sanitize_text_field(wp_unslash($_POST['body'])), true);
     485            $raw_body = json_decode(wp_unslash($_POST['body']), true); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_recursive below handles sanitization
    481486            if (is_array($raw_body)) {
    482487                $body = $this->sanitize_recursive($raw_body);
  • freelancebo-sentra-control/trunk/includes/class-sentra-api-client.php

    r3485668 r3488388  
    3333        $signature = hash_hmac('sha256', $sign_string, $this->api_secret);
    3434
     35        $sslverify = get_option('sentra_ssl_verify', '1') === '1';
     36
    3537        $args = [
    3638            'method'    => strtoupper($method),
    3739            'timeout'   => 15,
    38             'sslverify' => false,
    39             'reject_unsafe_urls' => false,
     40            'sslverify' => $sslverify,
     41            'reject_unsafe_urls' => $sslverify,
    4042            'headers'   => [
    4143                'Content-Type'    => 'application/json',
  • freelancebo-sentra-control/trunk/includes/class-sentra-event-queue.php

    r3487458 r3488388  
    9292    private function get_client_ip() {
    9393        $remote_addr = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'));
    94         $trusted_proxies = array_filter(array_map('trim', explode(',', get_option('sentra_trusted_proxies', ''))));
     94        $trusted_proxies = array_filter(
     95            array_map('trim', explode(',', get_option('sentra_trusted_proxies', ''))),
     96            function($ip) { return filter_var($ip, FILTER_VALIDATE_IP) !== false; }
     97        );
    9598
    9699        // Only trust proxy headers if request comes from a trusted proxy
  • freelancebo-sentra-control/trunk/includes/class-sentra-heartbeat.php

    r3488339 r3488388  
    6666        }
    6767
     68        // Anti-replay: reject duplicate signatures within 2 minutes
     69        $nonce_key = 'sentra_nonce_' . md5($signature);
     70        if (get_transient($nonce_key)) {
     71            return new WP_Error('auth_failed', __( 'Replay detected', 'freelancebo-sentra-control' ), ['status' => 401]);
     72        }
     73        set_transient($nonce_key, 1, 120);
     74
    6875        return true;
    6976    }
     
    7481     */
    7582    public function rest_run_pending( $request ) {
     83        // Rate limiting: 1 request per 10 seconds per IP
     84        $rate_key = 'sentra_rest_rate_' . md5(isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '');
     85        if (get_transient($rate_key)) {
     86            return new WP_REST_Response(['error' => 'Rate limited'], 429);
     87        }
     88        set_transient($rate_key, 1, 10);
     89
    7690        // Always send heartbeat when server pushes
    7791        $this->send_heartbeat();
     
    201215    }
    202216}
    203 
  • freelancebo-sentra-control/trunk/includes/modules/class-sentra-auto-patcher.php

    r3487530 r3488388  
    6161        }
    6262
     63        // Anti-replay: reject duplicate signatures within 2 minutes
     64        $nonce_key = 'sentra_nonce_' . md5($signature);
     65        if (get_transient($nonce_key)) {
     66            return new WP_Error('auth_failed', 'Replay detected', ['status' => 401]);
     67        }
     68        set_transient($nonce_key, 1, 120);
     69
    6370        return true;
    6471    }
     
    6875     */
    6976    public function rest_auto_patch($request) {
     77        // Rate limiting: 1 request per 10 seconds per IP
     78        $rate_key = 'sentra_rest_patch_rate_' . md5(isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '');
     79        if (get_transient($rate_key)) {
     80            return new WP_REST_Response(['error' => 'Rate limited'], 429);
     81        }
     82        set_transient($rate_key, 1, 10);
     83
    7084        $this->process_pending_patches();
    7185        return new WP_REST_Response(['status' => 'ok'], 200);
     
    99113     */
    100114    public function process_pending_patches() {
     115        if (get_option('sentra_auto_patch_enabled', '0') !== '1') {
     116            return; // Auto-patching disabled, admin must approve manually
     117        }
     118
    101119        $pending = $this->api->get('/agent/pending-patches');
    102120        if (!is_array($pending) || empty($pending) || isset($pending['error'])) {
     
    432450        if (!$this->validate_php_syntax($content)) {
    433451            return ['status' => 'error', 'message' => 'PHP syntax error after patch, not applied'];
     452        }
     453
     454        // Allow filtering/blocking of surgical patches
     455        $approved = apply_filters('sentra_surgical_patch_approved', true, $file_path, $content);
     456        if (!$approved) {
     457            return ['status' => 'skipped', 'message' => 'Patch requires manual approval'];
    434458        }
    435459
     
    727751                file_put_contents($index, "<?php // Silence is golden\n"); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
    728752            }
     753            // Add nginx protection note
     754            $nginx_note_file = $dir . '/.nginx-deny';
     755            if (!file_exists($nginx_note_file)) {
     756                $nginx_note = "# If using Nginx, add this to your server block:\n# location ~* /wp-content/(sentra-backups|sentra-quarantine)/ { deny all; }\n";
     757                @file_put_contents($nginx_note_file, $nginx_note); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
     758            }
    729759        }
    730760    }
  • freelancebo-sentra-control/trunk/includes/modules/class-sentra-firewall.php

    r3485789 r3488388  
    5151            $content_type = isset($_SERVER['CONTENT_TYPE']) ? sanitize_text_field(wp_unslash($_SERVER['CONTENT_TYPE'])) : '';
    5252            if (stripos($content_type, 'multipart/form-data') === false) {
    53                 $body = file_get_contents('php://input');
    54                 if (strlen($body) > 10000) {
    55                     $body = substr($body, 0, 10000);
     53                $content_length = isset($_SERVER['CONTENT_LENGTH']) ? intval($_SERVER['CONTENT_LENGTH']) : 0;
     54                if ($content_length > 10240) {
     55                    $body = ''; // Skip reading very large bodies
     56                } else {
     57                    $body = substr(@file_get_contents('php://input'), 0, 10240);
    5658                }
    5759            }
     
    121123            $content_type = isset($_SERVER['CONTENT_TYPE']) ? sanitize_text_field(wp_unslash($_SERVER['CONTENT_TYPE'])) : '';
    122124            if (stripos($content_type, 'multipart/form-data') === false) {
    123                 $body = file_get_contents('php://input');
    124                 if (strlen($body) > 10000) {
    125                     $body = substr($body, 0, 10000);
     125                $content_length = isset($_SERVER['CONTENT_LENGTH']) ? intval($_SERVER['CONTENT_LENGTH']) : 0;
     126                if ($content_length > 10240) {
     127                    $body = ''; // Skip reading very large bodies
     128                } else {
     129                    $body = substr(@file_get_contents('php://input'), 0, 10240);
    126130                }
    127131            }
     
    174178
    175179        if (isset($result['firewall_rules'])) {
    176             update_option('sentra_firewall_rules', $result['firewall_rules'], false);
     180            $rules = $result['firewall_rules'];
     181            if (is_array($rules)) {
     182                foreach ($rules as $key => $rule) {
     183                    if (isset($rule['pattern']) && @preg_match('/' . $rule['pattern'] . '/', '') === false) {
     184                        unset($rules[$key]); // Remove invalid regex
     185                    }
     186                    if (isset($rule['pattern']) && strlen($rule['pattern']) > 500) {
     187                        unset($rules[$key]); // Remove overly long patterns
     188                    }
     189                }
     190                $rules = array_values($rules); // Re-index
     191            }
     192            update_option('sentra_firewall_rules', $rules, false);
    177193        }
    178194        if (isset($result['blocked_ips'])) {
  • freelancebo-sentra-control/trunk/includes/modules/class-sentra-ip-blocker.php

    r3485648 r3488388  
    6969    private function get_client_ip() {
    7070        $remote_addr = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'));
    71         $trusted_proxies = array_filter(array_map('trim', explode(',', get_option('sentra_trusted_proxies', ''))));
     71        $trusted_proxies = array_filter(
     72            array_map('trim', explode(',', get_option('sentra_trusted_proxies', ''))),
     73            function($ip) { return filter_var($ip, FILTER_VALIDATE_IP) !== false; }
     74        );
    7275
    7376        // Only trust proxy headers if request comes from a trusted proxy
  • freelancebo-sentra-control/trunk/includes/modules/class-sentra-login-guard.php

    r3485648 r3488388  
    3030        }
    3131
    32         $attempts = get_transient('sentra_login_attempts_' . md5($ip));
    33         $user_attempts = get_transient('sentra_login_attempts_user_' . md5($username));
     32        $attempts = get_transient('sentra_login_attempts_' . wp_hash($ip));
     33        $user_attempts = get_transient('sentra_login_attempts_user_' . wp_hash($username));
    3434
    3535        if (($attempts && $attempts >= $this->max_attempts) || ($user_attempts && $user_attempts >= $this->max_attempts)) {
     
    5454    public function record_failed_login($username, $error = null) {
    5555        $ip = $this->get_client_ip();
    56         $key = 'sentra_login_attempts_' . md5($ip);
     56        $key = 'sentra_login_attempts_' . wp_hash($ip);
    5757        $attempts = (int) get_transient($key);
    5858        $attempts++;
    5959        set_transient($key, $attempts, $this->lockout_duration);
    6060
    61         $user_key = 'sentra_login_attempts_user_' . md5($username);
     61        $user_key = 'sentra_login_attempts_user_' . wp_hash($username);
    6262        $user_attempts = (int) get_transient($user_key);
    6363        $user_attempts++;
     
    126126    public function record_successful_login($user_login, $user) {
    127127        $ip = $this->get_client_ip();
    128         delete_transient('sentra_login_attempts_' . md5($ip));
    129         delete_transient('sentra_login_attempts_user_' . md5($user_login));
     128        delete_transient('sentra_login_attempts_' . wp_hash($ip));
     129        delete_transient('sentra_login_attempts_user_' . wp_hash($user_login));
    130130
    131131        // Deduplicate: skip if same user+IP already logged in recently (30 min)
     
    149149    private function get_client_ip() {
    150150        $remote_addr = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'));
    151         $trusted_proxies = array_filter(array_map('trim', explode(',', get_option('sentra_trusted_proxies', ''))));
     151        $trusted_proxies = array_filter(
     152            array_map('trim', explode(',', get_option('sentra_trusted_proxies', ''))),
     153            function($ip) { return filter_var($ip, FILTER_VALIDATE_IP) !== false; }
     154        );
    152155
    153156        // Only trust proxy headers if request comes from a trusted proxy
  • freelancebo-sentra-control/trunk/includes/modules/class-sentra-malware-scanner.php

    r3488330 r3488388  
    8888    ];
    8989
    90     private $chunk_size = 500; // files per tick
     90    private $chunk_size = 2000; // files per tick
    9191
    9292    public function __construct(Sentra_API_Client $api) {
  • freelancebo-sentra-control/trunk/readme.txt

    r3488339 r3488388  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.3.1
     7Stable tag: 2.4.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    7474
    7575== Changelog ==
     76
     77= 2.4.0 =
     78* Security: SSL verification now configurable with default ON (C3)
     79* Security: Auto-patching from server now disabled by default, requires admin approval (C4)
     80* Security: Added filter hook for surgical patch approval (C5)
     81* Security: HMAC nonce anti-replay protection on REST API endpoints (H8)
     82* Security: Firewall regex rules validated on sync (H10)
     83* Security: Nginx deny rule file added to backup/quarantine directories (H11)
     84* Security: Recursive sanitization on source_finding in auto-patch (M11)
     85* Security: Rate limiting on REST API endpoints (M14)
     86* Security: Login guard uses wp_hash instead of md5 for transient keys (M15)
     87* Security: Content length check before reading php://input in firewall (M16)
     88* Improved: Malware scanner file limit increased from 500 to 2000 (L1)
     89* Fixed: JSON body double-sanitization in AJAX proxy (L3)
     90* Security: Trusted proxy IPs validated with FILTER_VALIDATE_IP (L5)
    7691
    7792= 2.3.1 =
     
    209224== Upgrade Notice ==
    210225
     226= 2.4.0 =
     227Security hardening: SSL verify default ON, auto-patch requires approval, HMAC anti-replay, rate limiting, firewall regex validation. Recommended for all users.
     228
    211229= 2.3.1 =
    212230Fixes heartbeat reliability on low-traffic sites. Recommended for all users.
Note: See TracChangeset for help on using the changeset viewer.