From 36e29627f29d5517f30899d514140ce6c02ddf12 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 26 Oct 2026 15:00:03 +0700 Subject: [PATCH 1/5] Dev. Git. Added the Skills folder to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index eff5a4137..8ba6b35f7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ psalm.xml /lib/CleantalkSP/Common/FSWatcher/Storage/data/ /lib/CleantalkSP/Common/FSWatcher/logs/ /lib/CleantalkSP/Common/Helpers/composer.json -/tests/.phpunit.result.cache \ No newline at end of file +/tests/.phpunit.result.cache +/skills/ \ No newline at end of file From 3110895cb6d593e7c8250fdd393569148426325c Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 26 Oct 2026 17:07:00 +0700 Subject: [PATCH 2/5] Fix. FSW. Restriction of access to FSW, edits for wpms, restriction for file actions --- inc/spbc-admin.php | 2 +- .../SpbctWP/FSWatcher/Analyzer/Analyzer.php | 96 ++++----- .../SpbctWP/FSWatcher/Controller.php | 6 +- lib/CleantalkSP/SpbctWP/ListTable.php | 7 + lib/CleantalkSP/SpbctWP/Scanner/Cure.php | 4 +- lib/CleantalkSP/SpbctWP/Scanner/Helper.php | 11 ++ .../Scanner/ScannerActions/BackupsActions.php | 2 +- .../SpbctWP/Scanner/ScannerAjaxEndpoints.php | 16 +- .../SpbctWP/Settings/SettingsGeneralReact.php | 5 +- lib/CleantalkSP/SpbctWP/Views/Settings.php | 3 +- security-malware-firewall.php | 6 +- .../FSWatcher/AnalyzerViewFileTest.php | 185 ++++++++++++++++++ 12 files changed, 280 insertions(+), 63 deletions(-) create mode 100644 tests/lib/CleantalkSP/SpbctWP/FSWatcher/AnalyzerViewFileTest.php diff --git a/inc/spbc-admin.php b/inc/spbc-admin.php index 1a431d382..3a7e49ee1 100644 --- a/inc/spbc-admin.php +++ b/inc/spbc-admin.php @@ -848,7 +848,7 @@ function spbc_get_authorized_admins($direct_call = false) function spbc_action_shuffle_salts() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); global $spbc; $salts_array = array( diff --git a/lib/CleantalkSP/SpbctWP/FSWatcher/Analyzer/Analyzer.php b/lib/CleantalkSP/SpbctWP/FSWatcher/Analyzer/Analyzer.php index 530db2f71..5f3f66f86 100644 --- a/lib/CleantalkSP/SpbctWP/FSWatcher/Analyzer/Analyzer.php +++ b/lib/CleantalkSP/SpbctWP/FSWatcher/Analyzer/Analyzer.php @@ -4,7 +4,7 @@ use CleantalkSP\SpbctWP\FSWatcher\Controller; use CleantalkSP\SpbctWP\FSWatcher\Logger; -use CleantalkSP\SpbctWP\FSWatcher\Storage\FileStorage; +use CleantalkSP\SpbctWP\Scanner\Helper; class Analyzer { @@ -44,82 +44,88 @@ public static function getCompareResult() */ public static function getViewFile() { - $path = isset($_POST['fswatcher_file_path']) ? $_POST['fswatcher_file_path'] : false; + $path = isset($_POST['fswatcher_file_path']) ? wp_unslash($_POST['fswatcher_file_path']) : false; + $first = isset($_POST['fswatcher__first_date']) ? filter_var($_POST['fswatcher__first_date'], FILTER_VALIDATE_INT) : false; + $second = isset($_POST['fswatcher__second_date']) ? filter_var($_POST['fswatcher__second_date'], FILTER_VALIDATE_INT) : false; - $journals_first = isset($_POST['fswatcher__first_date']) ? $_POST['fswatcher__first_date'] : false; - $journals_second = isset($_POST['fswatcher__second_date']) ? $_POST['fswatcher__second_date'] : false; - $journals = array($journals_first, $journals_second); + if (!$path || !is_string($path)) { + throw new \Exception('File path is incorrect.'); + } - if (!$path || !is_file($path)) { + $real_path = realpath($path); + if ($real_path === false || !is_file($real_path)) { throw new \Exception('File path is incorrect.'); } - if (!$journals[0] || !$journals[1]) { + if (self::isWpConfigFile($path, $real_path) && !Helper::canViewWpConfigContents()) { + throw new \Exception(Helper::getWpConfigViewRestrictedMessage()); + } + + if (!$first || !$second) { throw new \Exception('Provided journals paths are incorrect.'); } - $path_found_in_journal = false; + $compare_result = self::getCompareResult(); - foreach ($journals as $journal_id) { - if ( - self::isFileOfFSWJournal($path, $journal_id) && - self::isFileOfFSWJournalAfterCompareJournals($path) - ) { - $path_found_in_journal = true; - break; - } + if (!is_array($compare_result) || !self::isPathInCompareResult($real_path, $compare_result)) { + throw new \Exception('The file is out of FSWatcher journals.'); } - if (!$path_found_in_journal) { - throw new \Exception('The file is out of FSWatcher journals.'); + $contents = file_get_contents($real_path); + if ($contents === false) { + throw new \Exception('File path is incorrect.'); } - return esc_html__(file_get_contents($path)); + return esc_html__($contents); } /** - * @param $path - * @param $journal_id + * Whether the file is the WordPress wp-config.php (site root or one level above), symlinks resolved. + * + * @param string $path Path as requested + * @param string $real_path Resolved path * @return bool */ - private static function isFileOfFSWJournal($path, $journal_id) + private static function isWpConfigFile($path, $real_path) { - $storage = new FileStorage(); - $journal_parsed = $storage->getJournal($journal_id); - $analyzer = new Analyzer(); - $journal_parsed = $analyzer->uncompress($journal_parsed, true); - if ( - strpos($journal_parsed, $path) !== false || - strpos($journal_parsed, stripslashes($path)) !== false - ) { + if (Helper::isWpConfigPath($path) || Helper::isWpConfigPath($real_path)) { return true; } + + foreach (array(ABSPATH . 'wp-config.php', ABSPATH . '../wp-config.php') as $config_file) { + $real_config_file = realpath($config_file); + if ($real_config_file !== false && $real_config_file === $real_path) { + return true; + } + } + return false; } /** - * @param $path + * Exact match of the resolved path against the paths of the journals diff. + * + * @param string $real_path + * @param array $compare_result Result of self::compare() * @return bool */ - private static function isFileOfFSWJournalAfterCompareJournals($path) + private static function isPathInCompareResult($real_path, array $compare_result) { - $journal_result_compare = self::getCompareResult(); - $journal_result_string = ''; - foreach ($journal_result_compare as $journal_result) { - if (count($journal_result) > 0) { - foreach ($journal_result as $value) { - $journal_result_string .= implode(' , ', $value); + foreach ($compare_result as $entries) { + if (!is_array($entries)) { + continue; + } + foreach ($entries as $entry) { + if (!isset($entry[0])) { + continue; + } + $entry_real_path = realpath((string) $entry[0]); + if ($entry_real_path !== false && $entry_real_path === $real_path) { + return true; } } } - if ( - strpos($journal_result_string, $path) !== false || - strpos($journal_result_string, stripslashes($path)) !== false - ) { - return true; - } - return false; } diff --git a/lib/CleantalkSP/SpbctWP/FSWatcher/Controller.php b/lib/CleantalkSP/SpbctWP/FSWatcher/Controller.php index b611a6143..85e16473c 100644 --- a/lib/CleantalkSP/SpbctWP/FSWatcher/Controller.php +++ b/lib/CleantalkSP/SpbctWP/FSWatcher/Controller.php @@ -122,7 +122,7 @@ private function checkRateLimit() public static function ajaxCompare() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); if (self::$debug) { Logger::log('run compare file system logs'); } @@ -141,7 +141,7 @@ public static function ajaxCompare() public function ajaxViewFile() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); if (self::$debug) { Logger::log('run view file method'); } @@ -165,7 +165,7 @@ public function ajaxViewFile() */ public function ajaxCreateSnapshot() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); if (self::$debug) { Logger::log('run scan file system'); } diff --git a/lib/CleantalkSP/SpbctWP/ListTable.php b/lib/CleantalkSP/SpbctWP/ListTable.php index bec47674b..179311c10 100644 --- a/lib/CleantalkSP/SpbctWP/ListTable.php +++ b/lib/CleantalkSP/SpbctWP/ListTable.php @@ -1080,6 +1080,13 @@ public static function ajaxRowActionHandler() spbc_check_ajax_referer('spbc_secret_nonce', 'security'); $action = Post::getString('add_action'); + + // Actions that change files of the whole network are for Super Admins only on a multisite + $network_wide_actions = array('delete', 'delete_quarantine', 'replace', 'quarantine', 'restore'); + if ( in_array($action, $network_wide_actions, true) && ! \CleantalkSP\SpbctWP\Scanner\Helper::canCurrentUserDoNetworkWideActions() ) { + wp_die('-1', 403); + } + $is_frontend_malware_action = $action === 'disapprove_page' || $action === 'approve_page'; if ( $action !== 'restore' && ! $is_frontend_malware_action && diff --git a/lib/CleantalkSP/SpbctWP/Scanner/Cure.php b/lib/CleantalkSP/SpbctWP/Scanner/Cure.php index dbfe6b2c2..ba83aa010 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/Cure.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/Cure.php @@ -331,7 +331,7 @@ private static function idsForBulkRequest($allowed_statuses) */ public static function cureSelectedAction() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); $ids = self::idsForBulkRequest(array('critical', 'cure_log')); @@ -389,7 +389,7 @@ public static function cureSelectedAction() public static function restoreSelectedAction() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); $ids = self::idsForBulkRequest(array('cure_log')); diff --git a/lib/CleantalkSP/SpbctWP/Scanner/Helper.php b/lib/CleantalkSP/SpbctWP/Scanner/Helper.php index bcb947c4e..86d0b5402 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/Helper.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/Helper.php @@ -332,6 +332,17 @@ public static function canViewWpConfigContents() return function_exists('is_super_admin') && is_super_admin(); } + /** + * Whether the current user may act on files shared by the whole network. + * On a multisite the file system is shared by all subsites, so only Super Admins may do it. + * + * @return bool + */ + public static function canCurrentUserDoNetworkWideActions() + { + return ! is_multisite() || self::canViewWpConfigContents(); + } + /** * @return string */ diff --git a/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php b/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php index ae57289f6..d293db2cc 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php @@ -15,7 +15,7 @@ class BackupsActions */ protected static function checkAjaxReferer($action = 'spbc_secret_nonce', $query_arg = 'security') { - spbc_check_ajax_referer($action, $query_arg); + spbc_check_ajax_referer($action, $query_arg, true, true, true); } /** diff --git a/lib/CleantalkSP/SpbctWP/Scanner/ScannerAjaxEndpoints.php b/lib/CleantalkSP/SpbctWP/Scanner/ScannerAjaxEndpoints.php index 588296764..a0e96764a 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/ScannerAjaxEndpoints.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/ScannerAjaxEndpoints.php @@ -33,7 +33,7 @@ public static function getLastScanInfo() */ public static function restoreFileFromBackup() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); $file_fast_hash = isset($_POST['file_fast_hash']) ? esc_sql($_POST['file_fast_hash']) : null; @@ -52,7 +52,7 @@ public static function restoreFileFromBackup() */ public static function cureFile() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); $file_fast_hash = isset($_POST['file_fast_hash']) ? esc_sql($_POST['file_fast_hash']) : null; @@ -139,7 +139,7 @@ public static function loadMoreScanLogs() */ public static function deleteFile() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); $time_start = microtime(true); @@ -218,7 +218,7 @@ public static function deleteFileFromAnalysisLog($direct_call = false) public static function restoreFromQuarantine($direct_call = false, $file_id = null) { if ( ! $direct_call) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); } $file_id = $direct_call ? $file_id : Post::get('file_id', 'hash'); @@ -277,7 +277,7 @@ public static function restoreFromQuarantine($direct_call = false, $file_id = nu public static function doQuarantineFile($direct_call = false, $file_id = null) { if ( ! $direct_call ) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); } $file_id = $direct_call ? $file_id : Post::get('file_id', 'hash'); @@ -302,7 +302,7 @@ public static function doQuarantineFile($direct_call = false, $file_id = null) public static function deleteFromQuarantine($direct_call = false, $file_id = null) { if ( ! $direct_call ) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); } $file_id = $direct_call ? $file_id : Post::get('file_id', 'hash'); @@ -327,7 +327,7 @@ public static function deleteFromQuarantine($direct_call = false, $file_id = nul public static function replaceFileWithOriginal($direct_call = false, $file_id = null) { if ( ! $direct_call) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); } $file_id = $direct_call ? $file_id : Post::get('file_id', 'hash'); @@ -371,7 +371,7 @@ public static function viewPage() */ public static function viewFile() { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + spbc_check_ajax_referer('spbc_secret_nonce', 'security', true, true, true); $output = FileSystemActions::viewFile(Post::get('file_id', 'hash')); diff --git a/lib/CleantalkSP/SpbctWP/Settings/SettingsGeneralReact.php b/lib/CleantalkSP/SpbctWP/Settings/SettingsGeneralReact.php index aed31a093..d83b310ab 100644 --- a/lib/CleantalkSP/SpbctWP/Settings/SettingsGeneralReact.php +++ b/lib/CleantalkSP/SpbctWP/Settings/SettingsGeneralReact.php @@ -233,7 +233,10 @@ private static function orderGroupElements($grouped_settings) $ordering_ruleset = self::getOrderingRuleset(); // UI-only field (not a saved option); rendered by React ActionShuffleSalts - $grouped_settings['auth_and_logging_in']['action_shuffle_salts'] = true; + // It rewrites the wp-config.php shared by the whole network, so on a multisite it is for Super Admins only + if ( \CleantalkSP\SpbctWP\Scanner\Helper::canCurrentUserDoNetworkWideActions() ) { + $grouped_settings['auth_and_logging_in']['action_shuffle_salts'] = true; + } foreach ($ordering_ruleset as $group_name => $ruleset) { $ordered = []; diff --git a/lib/CleantalkSP/SpbctWP/Views/Settings.php b/lib/CleantalkSP/SpbctWP/Views/Settings.php index 5c1f5c207..f2d30777d 100644 --- a/lib/CleantalkSP/SpbctWP/Views/Settings.php +++ b/lib/CleantalkSP/SpbctWP/Views/Settings.php @@ -108,7 +108,8 @@ public static function page() "displayDebug" => $display_debug, "isWPMSMainSite" => is_main_site(), "isSecFWEnabled" => (bool) $spbc->settings['secfw__enabled'], - "isFsWatcherEnabled" => (bool) $spbc->settings['scanner__fs_watcher'], + "isFsWatcherEnabled" => (bool) $spbc->settings['scanner__fs_watcher'] + && \CleantalkSP\SpbctWP\Scanner\Helper::canCurrentUserDoNetworkWideActions(), "isVulnerabilityCheckEnabled" => (bool) $spbc->settings['vulnerability_check__enable_cron'], ]; } diff --git a/security-malware-firewall.php b/security-malware-firewall.php index 2c0d17cca..ceee6d0f1 100644 --- a/security-malware-firewall.php +++ b/security-malware-firewall.php @@ -1434,9 +1434,10 @@ function spbc_PHP_logs__send() * @param string|false $query_arg query arg where to search a nonce * @param bool $die is need to stop the flow with 403 wp_die() * @param bool $strict_admins is need to check current user capabilities comply to admin's + * @param bool $network_wide on a multisite also require a Super Admin (for actions on the file system or wp-config.php shared by the whole network) * @return bool */ -function spbc_check_ajax_referer($action = -1, $query_arg = false, $die = true, $strict_admins = true) +function spbc_check_ajax_referer($action = -1, $query_arg = false, $die = true, $strict_admins = true, $network_wide = false) { $result = true; if (function_exists('check_ajax_referer')) { @@ -1450,6 +1451,9 @@ function spbc_check_ajax_referer($action = -1, $query_arg = false, $die = true, if ($result && $strict_admins) { $result = current_user_can('manage_options'); } + if ($result && $network_wide) { + $result = \CleantalkSP\SpbctWP\Scanner\Helper::canCurrentUserDoNetworkWideActions(); + } if (!$result && $die) { wp_die('-1', 403); } diff --git a/tests/lib/CleantalkSP/SpbctWP/FSWatcher/AnalyzerViewFileTest.php b/tests/lib/CleantalkSP/SpbctWP/FSWatcher/AnalyzerViewFileTest.php new file mode 100644 index 000000000..81c7abe32 --- /dev/null +++ b/tests/lib/CleantalkSP/SpbctWP/FSWatcher/AnalyzerViewFileTest.php @@ -0,0 +1,185 @@ +storage_backup = Controller::$storage; + $this->user_backup = get_current_user_id(); + Controller::$storage = FileStorage::class; + wp_set_current_user(1); + } + + protected function tearDown(): void + { + foreach ($this->files_to_remove as $file) { + if (is_file($file)) { + unlink($file); + } + } + $this->files_to_remove = array(); + Controller::$storage = $this->storage_backup; + wp_set_current_user($this->user_backup); + $_POST = array(); + parent::tearDown(); + } + + /** + * Site-root wp-config.php must not be readable through the viewer by a non Super Admin. + * @test + */ + public function testWpConfigInDiffIsDeniedWithoutSuperAdmin() + { + $wp_config = $this->prepareWpConfig(); + $this->writeJournals(array($wp_config => '1000'), array($wp_config => '2000')); + wp_set_current_user(0); + + $result = $this->view($wp_config); + + $this->assertArrayHasKey('error', $result, 'wp-config.php contents must not be returned'); + $this->assertStringContainsString('Super Admin', $result['error']); + } + + /** + * The same file stays viewable for a Super Admin. + * @test + */ + public function testWpConfigInDiffIsReadableForSuperAdmin() + { + $wp_config = $this->prepareWpConfig(); + $this->writeJournals(array($wp_config => '1000'), array($wp_config => '2000')); + wp_set_current_user(1); + + $result = $this->view($wp_config); + + $this->assertArrayHasKey('data', $result); + $this->assertStringContainsString('spbc-fsw-test-marker', $result['data']); + } + + /** + * Legit case: ordinary changed file is viewable. + * @test + */ + public function testChangedFileIsReadable() + { + $file = $this->makeTempFile('ordinary'); + $this->writeJournals(array($file => '1000'), array($file => '2000')); + + $result = $this->view($file); + + $this->assertArrayHasKey('data', $result); + $this->assertStringContainsString('spbc-fsw-test-marker', $result['data']); + } + + /** + * A file that exists but is not part of the diff must be rejected. + * @test + */ + public function testFileOutOfDiffIsDenied() + { + $file = $this->makeTempFile('victim'); + $other = $this->makeTempFile('other'); + $this->writeJournals(array($other => '1000'), array($other => '2000')); + + $result = $this->view($file); + + $this->assertArrayHasKey('error', $result); + $this->assertStringContainsString('out of FSWatcher journals', $result['error']); + } + + /** + * Path that is only a substring of a journal line must not pass. + * @test + */ + public function testPathThatIsOnlySubstringOfJournalEntryIsDenied() + { + $file = $this->makeTempFile('victim'); + $longer = $file . '.bak'; + $this->writeJournals(array($longer => '1000'), array($longer => '2000')); + + $result = $this->view($file); + + $this->assertArrayHasKey('error', $result); + $this->assertStringContainsString('out of FSWatcher journals', $result['error']); + } + + private function prepareWpConfig() + { + $wp_config = ABSPATH . 'wp-config.php'; + if (file_exists($wp_config)) { + $this->markTestSkipped('A real wp-config.php exists in ABSPATH, it is not touched by tests.'); + } + file_put_contents($wp_config, "files_to_remove[] = $wp_config; + + return $wp_config; + } + + private function makeTempFile($name) + { + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'spbc-fsw-' . $name . '-' . uniqid() . '.php'; + file_put_contents($path, "files_to_remove[] = $path; + + return $path; + } + + private function writeJournals(array $first, array $second) + { + global $spbc; + $dir = FileStorage::getJournalsPath(); + $key_hash = md5($spbc->settings['spbc_key']); + foreach (array(self::FIRST_JOURNAL => $first, self::SECOND_JOURNAL => $second) as $id => $rows) { + $csv = ''; + foreach ($rows as $path => $mtime) { + $csv .= '"' . $path . '",' . $mtime . "\n"; + } + $journal = $dir . $id . '_' . $key_hash . FileStorage::STATUS_COMPLETED . '.csv.gz'; + $gz = gzopen($journal, 'w9'); + gzwrite($gz, $csv); + gzclose($gz); + $this->files_to_remove[] = $journal; + } + } + + /** + * @return array{data?: string, error?: string} + */ + private function view($path) + { + $_POST = wp_slash(array( + 'fswatcher_file_path' => $path, + 'fswatcher__first_date' => (string) self::FIRST_JOURNAL, + 'fswatcher__second_date' => (string) self::SECOND_JOURNAL, + )); + + try { + return array('data' => Analyzer::getViewFile()); + } catch (\Exception $e) { + return array('error' => $e->getMessage()); + } + } +} From a7ea0cd33e5f90cbb76335c3c58b77e54477bfe8 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 26 Oct 2026 17:25:32 +0700 Subject: [PATCH 3/5] Fix. FSW. Limitation for actions of oscron and delete-trigger --- lib/CleantalkSP/SpbctWP/ListTable.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/CleantalkSP/SpbctWP/ListTable.php b/lib/CleantalkSP/SpbctWP/ListTable.php index 179311c10..456b1bd45 100644 --- a/lib/CleantalkSP/SpbctWP/ListTable.php +++ b/lib/CleantalkSP/SpbctWP/ListTable.php @@ -1081,8 +1081,17 @@ public static function ajaxRowActionHandler() $action = Post::getString('add_action'); - // Actions that change files of the whole network are for Super Admins only on a multisite - $network_wide_actions = array('delete', 'delete_quarantine', 'replace', 'quarantine', 'restore'); + // Actions that change files, OS crontab or DB triggers shared by the whole network are for Super Admins only on a multisite + $network_wide_actions = array( + 'delete', + 'delete_quarantine', + 'replace', + 'quarantine', + 'restore', + 'enable_oscron_task', + 'disable_oscron_task', + 'delete-trigger', + ); if ( in_array($action, $network_wide_actions, true) && ! \CleantalkSP\SpbctWP\Scanner\Helper::canCurrentUserDoNetworkWideActions() ) { wp_die('-1', 403); } From 2ff1bfb6cfa5ce63ccc0f21598ac6204bab28355 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 26 Oct 2026 17:47:10 +0700 Subject: [PATCH 4/5] Dev. Code. Review corrections --- lib/CleantalkSP/SpbctWP/ListTable.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/CleantalkSP/SpbctWP/ListTable.php b/lib/CleantalkSP/SpbctWP/ListTable.php index 456b1bd45..cc97bbbe6 100644 --- a/lib/CleantalkSP/SpbctWP/ListTable.php +++ b/lib/CleantalkSP/SpbctWP/ListTable.php @@ -1093,7 +1093,11 @@ public static function ajaxRowActionHandler() 'delete-trigger', ); if ( in_array($action, $network_wide_actions, true) && ! \CleantalkSP\SpbctWP\Scanner\Helper::canCurrentUserDoNetworkWideActions() ) { - wp_die('-1', 403); + wp_send_json(array( + 'temp_html' => '' + )); } $is_frontend_malware_action = $action === 'disapprove_page' || $action === 'approve_page'; From 3d6587c7c78e868d62e979afb946482d0f704171 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 26 Oct 2026 20:15:48 +0700 Subject: [PATCH 5/5] Dev. Activator. Switching to the wp_initialize_site hook when adding a new blog --- lib/CleantalkSP/SpbctWP/Activator.php | 7 +-- .../SpbctWP/ActivatorNewBlogTest.php | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/lib/CleantalkSP/SpbctWP/ActivatorNewBlogTest.php diff --git a/lib/CleantalkSP/SpbctWP/Activator.php b/lib/CleantalkSP/SpbctWP/Activator.php index e4d059bb4..25670ac56 100644 --- a/lib/CleantalkSP/SpbctWP/Activator.php +++ b/lib/CleantalkSP/SpbctWP/Activator.php @@ -22,7 +22,7 @@ public static function activation($network, $redirect = true) CDNHeadersChecker::sendCDNCheckerRequest(); - if ( current_action() !== 'wp_insert_site' ) { + if ( current_action() !== 'wp_initialize_site' ) { delete_option('spbc_deactivation_in_process'); } @@ -178,7 +178,8 @@ public static function addActionForNetworkBlogLegacy($wp_version) if ( version_compare($wp_version, '5.1.0', '<') ) { add_action('wpmu_new_blog', array(static::class, 'actionNewBlogLegacyMode'), 10, 6); } else { - add_action('wp_insert_site', array(static::class, 'actionNewBlog'), 10, 1); + // Core creates the blog tables on wp_initialize_site with priority 10, so run after it + add_action('wp_initialize_site', array(static::class, 'actionNewBlog'), 11, 1); } } @@ -215,7 +216,7 @@ public static function actionNewBlogLegacyMode($blog_id, $_user_id, $_domain, $_ * Wrapper for spbc_activation__new_blog() * * @param $new_site - * @psalm-suppress UnusedMethod - this method called via hook wp_insert_site/wpmu_new_blog + * @psalm-suppress UnusedMethod - this method called via hook wp_initialize_site/wpmu_new_blog */ public static function actionNewBlog($new_site) { diff --git a/tests/lib/CleantalkSP/SpbctWP/ActivatorNewBlogTest.php b/tests/lib/CleantalkSP/SpbctWP/ActivatorNewBlogTest.php new file mode 100644 index 000000000..f0eb0dccd --- /dev/null +++ b/tests/lib/CleantalkSP/SpbctWP/ActivatorNewBlogTest.php @@ -0,0 +1,52 @@ +assertFalse(has_action('wp_insert_site', array(Activator::class, 'actionNewBlog'))); + $this->assertSame(11, has_action('wp_initialize_site', array(Activator::class, 'actionNewBlog'))); + } + + /** + * @test + */ + public function testNewBlogGetsPluginDataOnNetworkActivePlugin() + { + if ( ! is_multisite() ) { + $this->markTestSkipped('Multisite only, run with WP_MULTISITE=1.'); + } + + // Core creates the blog tables here, the plugin must run later + $this->assertSame(10, has_action('wp_initialize_site', 'wp_initialize_site')); + + $plugin = 'security-malware-firewall/security-malware-firewall.php'; + $active_plugins_backup = get_site_option('active_sitewide_plugins'); + $active_plugins = is_array($active_plugins_backup) ? $active_plugins_backup : array(); + $active_plugins[$plugin] = time(); + update_site_option('active_sitewide_plugins', $active_plugins); + + $blog_id = wpmu_create_blog( + (string) get_network()->domain, + '/spbc-new-blog-' . uniqid() . '/', + 'SPBC new blog test', + 1 + ); + + try { + $this->assertIsInt($blog_id); + $this->assertNotFalse(get_blog_option($blog_id, 'spbc_data')); + } finally { + update_site_option('active_sitewide_plugins', $active_plugins_backup); + if (is_int($blog_id)) { + wp_delete_site($blog_id); + } + } + } +}