Plugin Directory

Changeset 3479541


Ignore:
Timestamp:
03/10/2026 08:52:04 PM (3 weeks ago)
Author:
jerryscg
Message:

Release 2.0.3

Location:
vulntitan
Files:
12 edited
5 copied

Legend:

Unmodified
Added
Removed
  • vulntitan/tags/2.0.3/CHANGELOG.md

    r3479449 r3479541  
    55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
    66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
     7
     8## [2.0.3] - 2026-03-10
     9### Fixed
     10- Reduced false positives for benign decode-only utilities (e.g., base64 + gzuncompress).
     11- Reduced false positives for safe `data:image/svg+xml;base64` payloads.
     12- Disabled auto-fix for low-risk malware findings to prevent accidental code removal.
    713
    814## [2.0.2] - 2026-03-10
  • vulntitan/tags/2.0.3/assets/js/malware-scanner.js

    r3469591 r3479541  
    195195            const applyFixLabel = VulnTitan.i18n.malware_fix_apply || 'Apply Fix';
    196196            const fixedLabel = VulnTitan.i18n.malware_fixed || 'Fixed';
    197             const fixDisabled = finding.fixed || !lineNumber;
    198             const fixStatus = finding.fix_message ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>` : '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>';
    199 
    200             return `
    201                 <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
    202                     <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
    203                         <span style="font-size:12px; color:#fda4af; font-weight:600;">
    204                             #${findingIndex + 1} • ${escapeHtml(lineLabel)}
    205                         </span>
    206                         <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
    207                             ${escapeHtml(severityLabel)}
    208                         </span>
    209                     </div>
    210                     ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
    211                     ${renderFindingPreviewLines(finding)}
    212                     <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     197            const fixAllowed = finding.fixable === true;
     198            const fixUnavailableLabel = VulnTitan.i18n.malware_fix_unavailable || 'Auto-fix not available for low-risk findings.';
     199            const fixDisabled = finding.fixed || !lineNumber || !fixAllowed;
     200            const fixStatus = finding.fix_message
     201                ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>`
     202                : (fixAllowed
     203                    ? '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>'
     204                    : `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;">${escapeHtml(fixUnavailableLabel)}</span>`);
     205            const fixButton = fixAllowed ? `
    213206                        <button
    214207                            type="button"
     
    221214                            ${escapeHtml(finding.fixed ? fixedLabel : applyFixLabel)}
    222215                        </button>
     216                    ` : '';
     217
     218            return `
     219                <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
     220                    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
     221                        <span style="font-size:12px; color:#fda4af; font-weight:600;">
     222                            #${findingIndex + 1} • ${escapeHtml(lineLabel)}
     223                        </span>
     224                        <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
     225                            ${escapeHtml(severityLabel)}
     226                        </span>
     227                    </div>
     228                    ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
     229                    ${renderFindingPreviewLines(finding)}
     230                    <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     231                        ${fixButton}
    223232                        ${fixStatus}
    224233                    </div>
  • vulntitan/tags/2.0.3/assets/js/malware-scanner.min.js

    r3469591 r3479541  
    195195            const applyFixLabel = VulnTitan.i18n.malware_fix_apply || 'Apply Fix';
    196196            const fixedLabel = VulnTitan.i18n.malware_fixed || 'Fixed';
    197             const fixDisabled = finding.fixed || !lineNumber;
    198             const fixStatus = finding.fix_message ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>` : '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>';
    199 
    200             return `
    201                 <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
    202                     <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
    203                         <span style="font-size:12px; color:#fda4af; font-weight:600;">
    204                             #${findingIndex + 1} • ${escapeHtml(lineLabel)}
    205                         </span>
    206                         <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
    207                             ${escapeHtml(severityLabel)}
    208                         </span>
    209                     </div>
    210                     ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
    211                     ${renderFindingPreviewLines(finding)}
    212                     <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     197            const fixAllowed = finding.fixable === true;
     198            const fixUnavailableLabel = VulnTitan.i18n.malware_fix_unavailable || 'Auto-fix not available for low-risk findings.';
     199            const fixDisabled = finding.fixed || !lineNumber || !fixAllowed;
     200            const fixStatus = finding.fix_message
     201                ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>`
     202                : (fixAllowed
     203                    ? '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>'
     204                    : `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;">${escapeHtml(fixUnavailableLabel)}</span>`);
     205            const fixButton = fixAllowed ? `
    213206                        <button
    214207                            type="button"
     
    221214                            ${escapeHtml(finding.fixed ? fixedLabel : applyFixLabel)}
    222215                        </button>
     216                    ` : '';
     217
     218            return `
     219                <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
     220                    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
     221                        <span style="font-size:12px; color:#fda4af; font-weight:600;">
     222                            #${findingIndex + 1} • ${escapeHtml(lineLabel)}
     223                        </span>
     224                        <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
     225                            ${escapeHtml(severityLabel)}
     226                        </span>
     227                    </div>
     228                    ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
     229                    ${renderFindingPreviewLines(finding)}
     230                    <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     231                        ${fixButton}
    223232                        ${fixStatus}
    224233                    </div>
  • vulntitan/tags/2.0.3/includes/Admin/Admin.php

    r3469591 r3479541  
    241241                'malware_fix_confirm' => esc_html__('Apply safe fix for this suspicious line? A backup will be created.', 'vulntitan'),
    242242                'malware_fix_no_line' => esc_html__('This finding has no exact line, so auto-fix is not available.', 'vulntitan'),
     243                'malware_fix_unavailable' => esc_html__('Auto-fix is disabled for low-risk findings.', 'vulntitan'),
    243244                'malware_fix_success' => esc_html__('Fix applied.', 'vulntitan'),
    244245                'malware_fix_error' => esc_html__('Auto-fix failed.', 'vulntitan'),
  • vulntitan/tags/2.0.3/includes/Admin/Ajax.php

    r3469591 r3479541  
    229229        }
    230230
     231        if (!$this->isFixablePatternList($patterns)) {
     232            wp_send_json_error(['message' => esc_html__('Auto-fix is disabled for low-risk findings.', 'vulntitan')]);
     233        }
     234
    231235        $relativePath = ltrim(str_replace('\\', '/', $filePath), '/');
    232236        $absolutePath = ABSPATH . $relativePath;
     
    421425
    422426        return sprintf('/* VulnTitan quarantine: suspicious code removed at line %d%s */', $lineNumber, $suffix);
     427    }
     428
     429    protected function isFixablePatternList(string $patterns): bool
     430    {
     431        $items = array_filter(array_map('trim', explode(',', strtolower($patterns))));
     432        if (empty($items)) {
     433            return false;
     434        }
     435
     436        $fixablePatterns = [
     437            'eval', 'assert', 'create_function', 'preg_replace_eval',
     438            'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open',
     439            'include_user_input', 'file_put_contents', 'fwrite', 'move_uploaded_file',
     440            'js_function_ctor', 'js_eval_accessor', 'js_timer_string',
     441            'eval_base64_chain', 'decode_execution_chain', 'user_input_execution_chain',
     442            'encoded_file_write_chain', 'upload_input_chain',
     443        ];
     444
     445        foreach ($items as $pattern) {
     446            if (in_array($pattern, $fixablePatterns, true)) {
     447                return true;
     448            }
     449        }
     450
     451        return false;
    423452    }
    424453
  • vulntitan/tags/2.0.3/includes/Services/MalwareScanner.php

    r3479449 r3479541  
    151151            $hasExecution = !empty(array_intersect($executionPatterns, $matchedPatterns));
    152152            $hasFileWrite = !empty(array_intersect(['file_put_contents', 'fwrite', 'move_uploaded_file'], $matchedPatterns));
    153             $hasBenignDataImage = $this->isBenignDataImageLine($lineClean);
     153            $hasBenignDataImage = $this->isBenignDataImageLine($lineClean) || $this->isBenignSvgDataUriLine($lineClean);
    154154            $hasSerializedBlob = $this->isLikelySerializedDataLine($lineClean);
     155            $hasInput = in_array('superglobal_input', $matchedPatterns, true) || in_array('include_user_input', $matchedPatterns, true);
     156            $hasBase64Blob = in_array('base64_blob', $matchedPatterns, true);
    155157
    156158            // Reduce false positives for base64-decoded signatures/keys/certs.
     
    166168            }
    167169
     170            // Reduce false positives for decode-only utilities without execution or file writes.
     171            if (
     172                !$hasBase64Blob
     173                && !$hasExecution
     174                && !$hasFileWrite
     175                && !$hasInput
     176                && !empty($matchedPatterns)
     177                && empty(array_diff($matchedPatterns, $decodePatterns))
     178            ) {
     179                $score = max(0, $score - 1);
     180            }
     181
    168182            // Combo detection
    169183            if (in_array('eval', $matchedPatterns) && in_array('base64_decode', $matchedPatterns)) {
     
    264278    }
    265279
     280    protected function isBenignSvgDataUriLine(string $line): bool
     281    {
     282        if (!preg_match('/data:image\\/svg\\+xml;base64,([A-Za-z0-9+\\/=]+)/i', $line, $matches)) {
     283            return false;
     284        }
     285
     286        $payload = $matches[1] ?? '';
     287        if ($payload === '' || strlen($payload) > 20000) {
     288            return false;
     289        }
     290
     291        $decoded = base64_decode($payload, true);
     292        if ($decoded === false) {
     293            return false;
     294        }
     295
     296        $lower = strtolower($decoded);
     297        if (strpos($lower, '<svg') === false) {
     298            return false;
     299        }
     300
     301        if (preg_match('/<script|\\bon\\w+\\s*=|foreignobject|<iframe|<object|<embed|javascript:/i', $decoded)) {
     302            return false;
     303        }
     304
     305        return true;
     306    }
     307
    266308    protected function isLikelySerializedDataLine(string $line): bool
    267309    {
     
    301343                'patterns'   => $issue['patterns'],
    302344                'code'       => $code,
     345                'fixable'    => $this->isFixableFinding($issue['patterns'] ?? [], $severity),
    303346                'preview'    => $issue['preview'] ?? [],
    304347            ];
     
    359402        return 'VulnTitan';
    360403    }
     404
     405    protected function isFixableFinding(array $patterns, string $severity): bool
     406    {
     407        if (strtolower($severity) === 'low') {
     408            return false;
     409        }
     410
     411        $fixablePatterns = [
     412            'eval', 'assert', 'create_function', 'preg_replace_eval',
     413            'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open',
     414            'include_user_input', 'file_put_contents', 'fwrite', 'move_uploaded_file',
     415            'js_function_ctor', 'js_eval_accessor', 'js_timer_string',
     416            'eval_base64_chain', 'decode_execution_chain', 'user_input_execution_chain',
     417            'encoded_file_write_chain', 'upload_input_chain',
     418        ];
     419
     420        foreach ($patterns as $pattern) {
     421            if (in_array($pattern, $fixablePatterns, true)) {
     422                return true;
     423            }
     424        }
     425
     426        return false;
     427    }
    361428}
  • vulntitan/tags/2.0.3/readme.txt

    r3479449 r3479541  
    44Tested up to: 6.9
    55Requires PHP: 7.4
    6 Stable tag: 2.0.2
     6Stable tag: 2.0.3
    77License: GPLv2
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    129129== Changelog ==
    130130
     131= v2.0.3 - 10 Mar, 2026 =
     132* Reduced false positives for benign decode-only utilities (e.g., base64 + gzuncompress).
     133* Reduced false positives for safe data:image/svg+xml;base64 payloads.
     134* Disabled auto-fix for low-risk malware findings to prevent accidental code removal.
     135
    131136= v2.0.2 - 10 Mar, 2026 =
    132137* Reduced malware scanner false positives for base64-decoded signature and key material.
  • vulntitan/tags/2.0.3/vulntitan.php

    r3479449 r3479541  
    44 * Plugin URI: https://vulntitan.com/vulntitan/
    55 * Description: VulnTitan is a lightweight WordPress security plugin with vulnerability scanning, malware detection, file integrity monitoring, and a built-in firewall with WAF payload rules and login protection.
    6  * Version: 2.0.2
     6 * Version: 2.0.3
    77 * Author: Jaroslav Svetlik
    88 * Author URI: https://vulntitan.com
     
    3030
    3131// Define plugin constants
    32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.2');
     32define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.3');
    3333define('VULNTITAN_PLUGIN_BASENAME', plugin_basename(__FILE__));
    3434define('VULNTITAN_PLUGIN_DIR', untrailingslashit(plugin_dir_path(__FILE__)));
  • vulntitan/trunk/CHANGELOG.md

    r3479449 r3479541  
    55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
    66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
     7
     8## [2.0.3] - 2026-03-10
     9### Fixed
     10- Reduced false positives for benign decode-only utilities (e.g., base64 + gzuncompress).
     11- Reduced false positives for safe `data:image/svg+xml;base64` payloads.
     12- Disabled auto-fix for low-risk malware findings to prevent accidental code removal.
    713
    814## [2.0.2] - 2026-03-10
  • vulntitan/trunk/assets/js/malware-scanner.js

    r3469591 r3479541  
    195195            const applyFixLabel = VulnTitan.i18n.malware_fix_apply || 'Apply Fix';
    196196            const fixedLabel = VulnTitan.i18n.malware_fixed || 'Fixed';
    197             const fixDisabled = finding.fixed || !lineNumber;
    198             const fixStatus = finding.fix_message ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>` : '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>';
    199 
    200             return `
    201                 <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
    202                     <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
    203                         <span style="font-size:12px; color:#fda4af; font-weight:600;">
    204                             #${findingIndex + 1} • ${escapeHtml(lineLabel)}
    205                         </span>
    206                         <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
    207                             ${escapeHtml(severityLabel)}
    208                         </span>
    209                     </div>
    210                     ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
    211                     ${renderFindingPreviewLines(finding)}
    212                     <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     197            const fixAllowed = finding.fixable === true;
     198            const fixUnavailableLabel = VulnTitan.i18n.malware_fix_unavailable || 'Auto-fix not available for low-risk findings.';
     199            const fixDisabled = finding.fixed || !lineNumber || !fixAllowed;
     200            const fixStatus = finding.fix_message
     201                ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>`
     202                : (fixAllowed
     203                    ? '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>'
     204                    : `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;">${escapeHtml(fixUnavailableLabel)}</span>`);
     205            const fixButton = fixAllowed ? `
    213206                        <button
    214207                            type="button"
     
    221214                            ${escapeHtml(finding.fixed ? fixedLabel : applyFixLabel)}
    222215                        </button>
     216                    ` : '';
     217
     218            return `
     219                <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
     220                    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
     221                        <span style="font-size:12px; color:#fda4af; font-weight:600;">
     222                            #${findingIndex + 1} • ${escapeHtml(lineLabel)}
     223                        </span>
     224                        <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
     225                            ${escapeHtml(severityLabel)}
     226                        </span>
     227                    </div>
     228                    ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
     229                    ${renderFindingPreviewLines(finding)}
     230                    <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     231                        ${fixButton}
    223232                        ${fixStatus}
    224233                    </div>
  • vulntitan/trunk/assets/js/malware-scanner.min.js

    r3469591 r3479541  
    195195            const applyFixLabel = VulnTitan.i18n.malware_fix_apply || 'Apply Fix';
    196196            const fixedLabel = VulnTitan.i18n.malware_fixed || 'Fixed';
    197             const fixDisabled = finding.fixed || !lineNumber;
    198             const fixStatus = finding.fix_message ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>` : '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>';
    199 
    200             return `
    201                 <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
    202                     <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
    203                         <span style="font-size:12px; color:#fda4af; font-weight:600;">
    204                             #${findingIndex + 1} • ${escapeHtml(lineLabel)}
    205                         </span>
    206                         <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
    207                             ${escapeHtml(severityLabel)}
    208                         </span>
    209                     </div>
    210                     ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
    211                     ${renderFindingPreviewLines(finding)}
    212                     <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     197            const fixAllowed = finding.fixable === true;
     198            const fixUnavailableLabel = VulnTitan.i18n.malware_fix_unavailable || 'Auto-fix not available for low-risk findings.';
     199            const fixDisabled = finding.fixed || !lineNumber || !fixAllowed;
     200            const fixStatus = finding.fix_message
     201                ? `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:${finding.fix_error ? '#fca5a5' : '#86efac'};">${escapeHtml(finding.fix_message)}</span>`
     202                : (fixAllowed
     203                    ? '<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;"></span>'
     204                    : `<span class="vulntitan-malware-fix-feedback" style="font-size:11px; color:#94a3b8;">${escapeHtml(fixUnavailableLabel)}</span>`);
     205            const fixButton = fixAllowed ? `
    213206                        <button
    214207                            type="button"
     
    221214                            ${escapeHtml(finding.fixed ? fixedLabel : applyFixLabel)}
    222215                        </button>
     216                    ` : '';
     217
     218            return `
     219                <div style="border:1px solid #3b2020; border-radius:6px; background:#161017; padding:8px 10px;">
     220                    <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;">
     221                        <span style="font-size:12px; color:#fda4af; font-weight:600;">
     222                            #${findingIndex + 1} • ${escapeHtml(lineLabel)}
     223                        </span>
     224                        <span style="display:inline-block; padding:2px 8px; border:1px solid ${severityColor}; border-radius:999px; font-size:11px; color:${severityColor}; font-weight:700; letter-spacing:0.03em;">
     225                            ${escapeHtml(severityLabel)}
     226                        </span>
     227                    </div>
     228                    ${patterns ? `<div style="margin-top:6px; font-size:11px; color:#cbd5e1;">${escapeHtml(patterns)}</div>` : ''}
     229                    ${renderFindingPreviewLines(finding)}
     230                    <div class="vulntitan-malware-fix-actions" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
     231                        ${fixButton}
    223232                        ${fixStatus}
    224233                    </div>
  • vulntitan/trunk/includes/Admin/Admin.php

    r3469591 r3479541  
    241241                'malware_fix_confirm' => esc_html__('Apply safe fix for this suspicious line? A backup will be created.', 'vulntitan'),
    242242                'malware_fix_no_line' => esc_html__('This finding has no exact line, so auto-fix is not available.', 'vulntitan'),
     243                'malware_fix_unavailable' => esc_html__('Auto-fix is disabled for low-risk findings.', 'vulntitan'),
    243244                'malware_fix_success' => esc_html__('Fix applied.', 'vulntitan'),
    244245                'malware_fix_error' => esc_html__('Auto-fix failed.', 'vulntitan'),
  • vulntitan/trunk/includes/Admin/Ajax.php

    r3469591 r3479541  
    229229        }
    230230
     231        if (!$this->isFixablePatternList($patterns)) {
     232            wp_send_json_error(['message' => esc_html__('Auto-fix is disabled for low-risk findings.', 'vulntitan')]);
     233        }
     234
    231235        $relativePath = ltrim(str_replace('\\', '/', $filePath), '/');
    232236        $absolutePath = ABSPATH . $relativePath;
     
    421425
    422426        return sprintf('/* VulnTitan quarantine: suspicious code removed at line %d%s */', $lineNumber, $suffix);
     427    }
     428
     429    protected function isFixablePatternList(string $patterns): bool
     430    {
     431        $items = array_filter(array_map('trim', explode(',', strtolower($patterns))));
     432        if (empty($items)) {
     433            return false;
     434        }
     435
     436        $fixablePatterns = [
     437            'eval', 'assert', 'create_function', 'preg_replace_eval',
     438            'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open',
     439            'include_user_input', 'file_put_contents', 'fwrite', 'move_uploaded_file',
     440            'js_function_ctor', 'js_eval_accessor', 'js_timer_string',
     441            'eval_base64_chain', 'decode_execution_chain', 'user_input_execution_chain',
     442            'encoded_file_write_chain', 'upload_input_chain',
     443        ];
     444
     445        foreach ($items as $pattern) {
     446            if (in_array($pattern, $fixablePatterns, true)) {
     447                return true;
     448            }
     449        }
     450
     451        return false;
    423452    }
    424453
  • vulntitan/trunk/includes/Services/MalwareScanner.php

    r3479449 r3479541  
    151151            $hasExecution = !empty(array_intersect($executionPatterns, $matchedPatterns));
    152152            $hasFileWrite = !empty(array_intersect(['file_put_contents', 'fwrite', 'move_uploaded_file'], $matchedPatterns));
    153             $hasBenignDataImage = $this->isBenignDataImageLine($lineClean);
     153            $hasBenignDataImage = $this->isBenignDataImageLine($lineClean) || $this->isBenignSvgDataUriLine($lineClean);
    154154            $hasSerializedBlob = $this->isLikelySerializedDataLine($lineClean);
     155            $hasInput = in_array('superglobal_input', $matchedPatterns, true) || in_array('include_user_input', $matchedPatterns, true);
     156            $hasBase64Blob = in_array('base64_blob', $matchedPatterns, true);
    155157
    156158            // Reduce false positives for base64-decoded signatures/keys/certs.
     
    166168            }
    167169
     170            // Reduce false positives for decode-only utilities without execution or file writes.
     171            if (
     172                !$hasBase64Blob
     173                && !$hasExecution
     174                && !$hasFileWrite
     175                && !$hasInput
     176                && !empty($matchedPatterns)
     177                && empty(array_diff($matchedPatterns, $decodePatterns))
     178            ) {
     179                $score = max(0, $score - 1);
     180            }
     181
    168182            // Combo detection
    169183            if (in_array('eval', $matchedPatterns) && in_array('base64_decode', $matchedPatterns)) {
     
    264278    }
    265279
     280    protected function isBenignSvgDataUriLine(string $line): bool
     281    {
     282        if (!preg_match('/data:image\\/svg\\+xml;base64,([A-Za-z0-9+\\/=]+)/i', $line, $matches)) {
     283            return false;
     284        }
     285
     286        $payload = $matches[1] ?? '';
     287        if ($payload === '' || strlen($payload) > 20000) {
     288            return false;
     289        }
     290
     291        $decoded = base64_decode($payload, true);
     292        if ($decoded === false) {
     293            return false;
     294        }
     295
     296        $lower = strtolower($decoded);
     297        if (strpos($lower, '<svg') === false) {
     298            return false;
     299        }
     300
     301        if (preg_match('/<script|\\bon\\w+\\s*=|foreignobject|<iframe|<object|<embed|javascript:/i', $decoded)) {
     302            return false;
     303        }
     304
     305        return true;
     306    }
     307
    266308    protected function isLikelySerializedDataLine(string $line): bool
    267309    {
     
    301343                'patterns'   => $issue['patterns'],
    302344                'code'       => $code,
     345                'fixable'    => $this->isFixableFinding($issue['patterns'] ?? [], $severity),
    303346                'preview'    => $issue['preview'] ?? [],
    304347            ];
     
    359402        return 'VulnTitan';
    360403    }
     404
     405    protected function isFixableFinding(array $patterns, string $severity): bool
     406    {
     407        if (strtolower($severity) === 'low') {
     408            return false;
     409        }
     410
     411        $fixablePatterns = [
     412            'eval', 'assert', 'create_function', 'preg_replace_eval',
     413            'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open',
     414            'include_user_input', 'file_put_contents', 'fwrite', 'move_uploaded_file',
     415            'js_function_ctor', 'js_eval_accessor', 'js_timer_string',
     416            'eval_base64_chain', 'decode_execution_chain', 'user_input_execution_chain',
     417            'encoded_file_write_chain', 'upload_input_chain',
     418        ];
     419
     420        foreach ($patterns as $pattern) {
     421            if (in_array($pattern, $fixablePatterns, true)) {
     422                return true;
     423            }
     424        }
     425
     426        return false;
     427    }
    361428}
  • vulntitan/trunk/readme.txt

    r3479449 r3479541  
    44Tested up to: 6.9
    55Requires PHP: 7.4
    6 Stable tag: 2.0.2
     6Stable tag: 2.0.3
    77License: GPLv2
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    129129== Changelog ==
    130130
     131= v2.0.3 - 10 Mar, 2026 =
     132* Reduced false positives for benign decode-only utilities (e.g., base64 + gzuncompress).
     133* Reduced false positives for safe data:image/svg+xml;base64 payloads.
     134* Disabled auto-fix for low-risk malware findings to prevent accidental code removal.
     135
    131136= v2.0.2 - 10 Mar, 2026 =
    132137* Reduced malware scanner false positives for base64-decoded signature and key material.
  • vulntitan/trunk/vulntitan.php

    r3479449 r3479541  
    44 * Plugin URI: https://vulntitan.com/vulntitan/
    55 * Description: VulnTitan is a lightweight WordPress security plugin with vulnerability scanning, malware detection, file integrity monitoring, and a built-in firewall with WAF payload rules and login protection.
    6  * Version: 2.0.2
     6 * Version: 2.0.3
    77 * Author: Jaroslav Svetlik
    88 * Author URI: https://vulntitan.com
     
    3030
    3131// Define plugin constants
    32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.2');
     32define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.3');
    3333define('VULNTITAN_PLUGIN_BASENAME', plugin_basename(__FILE__));
    3434define('VULNTITAN_PLUGIN_DIR', untrailingslashit(plugin_dir_path(__FILE__)));
Note: See TracChangeset for help on using the changeset viewer.