$value, 'expires' => $expiration > 0 ? time() + $expiration : 0]; return true; } function get_transient(string $key): mixed { $row = $GLOBALS['aksrt_security_transients'][$key] ?? null; if (!is_array($row)) return false; if (!empty($row['expires']) && $row['expires'] <= time()) { unset($GLOBALS['aksrt_security_transients'][$key]); return false; } return $row['value'] ?? false; } function delete_transient(string $key): bool { unset($GLOBALS['aksrt_security_transients'][$key]); return true; } function site_security_check(bool $condition, string $message): void { if (!$condition) throw new \RuntimeException($message); } function site_security_brush_minute_key(string $scope, string $subject): string { return sprintf('aksrt_brush_%s_m_%s_%s%d', $scope, md5($subject), wp_date('YmdH'), intdiv((int) wp_date('i'), 10)); } function site_security_brush_day_key(string $scope, string $subject): string { return sprintf('aksrt_brush_%s_d_%s_%s', $scope, md5($subject), wp_date('Ymd')); } require dirname(__DIR__, 2) . '/src/Settings/ExtensionRuntime.php'; require dirname(__DIR__, 2) . '/src/Extensions/SecurityHardening.php'; require dirname(__DIR__, 2) . '/src/Extensions/SendLimit.php'; require dirname(__DIR__, 2) . '/inc/helpers/aksrt-phone.php'; require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsIP.php'; require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsAudit.php'; require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsPush.php'; require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSecurity.php'; require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSystem.php'; require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensions.php'; } namespace AKSRT\Settings { final class Settings { public static function get_all(): array { return $GLOBALS['aksrt_security_settings']; } } } namespace { use AKSRT\Admin\Schema\SchemaExtensions; use AKSRT\Extensions\SecurityHardening; use AKSRT\Extensions\SendLimit; use AKSRT\Settings\ExtensionRuntime; // 1) Phone masking: domestic hides 4 digits, international hides length-7 (min 4), short numbers are unmaskable. $domestic = aksrt_phone_mask_parts('13812341234'); site_security_check($domestic !== null, 'domestic number should be maskable'); site_security_check($domestic['prefix'] === '138' && $domestic['hidden'] === '1234' && $domestic['suffix'] === '1234', 'domestic mask split is wrong'); site_security_check($domestic['hidden_length'] === 4, 'domestic mask should hide 4 digits'); site_security_check(aksrt_phone_mask_display('13812341234') === '138****1234', 'domestic masked display is wrong'); $intl = aksrt_phone_mask_parts('+8613812341234'); site_security_check($intl !== null, 'international number should be maskable'); site_security_check($intl['hidden_length'] === 6, 'international mask should hide length-7 digits'); site_security_check($intl['hidden'] === '381234', 'international hidden segment is wrong'); site_security_check($intl['leading_plus'] === true, 'leading plus flag is wrong'); site_security_check(aksrt_phone_mask_display('+8613812341234') === '+861******1234', 'international masked display is wrong'); $eight = aksrt_phone_mask_parts('12345678'); site_security_check($eight !== null && $eight['hidden_length'] === 4 && $eight['suffix'] === '8', 'short mask should still hide 4 digits with a 1-digit suffix'); site_security_check(aksrt_phone_mask_parts('1234567') === null, 'numbers with fewer than 8 digits must be unmaskable'); site_security_check(aksrt_phone_mask_parts('') === null, 'empty number must be unmaskable'); site_security_check(aksrt_phone_mask_display('1234567') === '1234567', 'unmaskable numbers must display raw'); // 2) Brush limits: 10-minute and daily caps, 0 = unlimited, admin exempt handled by callers. $allowed = SecurityHardening::brush_allow('post', '12', 2, 0); site_security_check($allowed === true, 'first post within cap was blocked'); site_security_check(SecurityHardening::brush_allow('post', '12', 2, 0) === true, 'second post within cap was blocked'); site_security_check((int) get_transient(site_security_brush_minute_key('post', '12')) === 2, 'post attempts were not counted'); site_security_check(SecurityHardening::brush_allow('post', '12', 2, 0) !== true, 'post 10-minute cap was not enforced'); $GLOBALS['aksrt_security_transients'] = []; site_security_check(SecurityHardening::brush_allow('post', '12', 0, 2) === true, 'first daily post was blocked'); site_security_check(SecurityHardening::brush_allow('post', '12', 0, 2) === true, 'second daily post was blocked'); site_security_check(SecurityHardening::brush_allow('post', '12', 0, 2) !== true, 'post daily cap was not enforced'); $GLOBALS['aksrt_security_transients'] = []; site_security_check(SecurityHardening::brush_allow('order', '203.0.113.9', 0, 0) === true, 'disabled caps should never block'); site_security_check(get_transient(site_security_brush_minute_key('order', '203.0.113.9')) === false, 'disabled caps should not count attempts'); // 3) Error frequency: silent under 5 failures, 30s window 5-9, 180s window at 10+. site_security_check(SecurityHardening::error_frequency_wait() === 0, 'error frequency should be silent with no failures'); for ($failure = 0; $failure < 4; $failure++) { SecurityHardening::error_frequency_record(); } site_security_check(SecurityHardening::error_frequency_wait() === 0, 'error frequency limited before reaching 5 failures'); SecurityHardening::error_frequency_record(); $wait = SecurityHardening::error_frequency_wait(); site_security_check($wait > 0 && $wait <= 30, 'error frequency did not apply the 30s window after 5 failures'); for ($failure = 0; $failure < 5; $failure++) { SecurityHardening::error_frequency_record(); } $long_wait = SecurityHardening::error_frequency_wait(); site_security_check($long_wait > 30 && $long_wait <= 180, 'error frequency did not apply the 180s window after 10 failures'); SecurityHardening::error_frequency_clear(); site_security_check(SecurityHardening::error_frequency_wait() === 0, 'error frequency clear did not reset the counter'); // Disabling the switch must silence the limit. $GLOBALS['aksrt_security_settings']['security_error_frequency_limit'] = false; SecurityHardening::error_frequency_record(); site_security_check(SecurityHardening::error_frequency_wait() === 0, 'disabling security_error_frequency_limit did not silence the limiter'); $GLOBALS['aksrt_security_settings'] = []; // 4) Send limit: cooldown, recipient hourly/daily, IP 10-minute/daily caps; 0 = unlimited. $GLOBALS['aksrt_security_transients'] = []; $first = SendLimit::check('user@example.test'); site_security_check($first['ok'] === true, 'first verification send was blocked'); site_security_check(SendLimit::record('user@example.test') === 60, 'send limit did not return the cooldown'); $second = SendLimit::check('user@example.test'); site_security_check($second['ok'] === false && $second['remaining'] > 0, 'send cooldown was not enforced'); site_security_check(SendLimit::check('other@example.test')['ok'] === true, 'send cooldown leaked across recipients'); $GLOBALS['aksrt_security_transients'] = []; for ($send = 0; $send < 15; $send++) { site_security_check(SendLimit::record('user@example.test') === 60, 'send record failed'); } site_security_check(SendLimit::check('user@example.test')['ok'] === false, 'recipient hourly cap was not enforced'); site_security_check(SendLimit::check('user2@example.test')['ok'] === true, 'recipient hourly cap leaked across recipients'); $GLOBALS['aksrt_security_transients'] = []; for ($send = 0; $send < 20; $send++) { SendLimit::record('user@example.test'); } site_security_check(SendLimit::check('user2@example.test')['ok'] === false, 'IP 10-minute cap was not enforced'); $GLOBALS['aksrt_security_transients'] = []; $GLOBALS['aksrt_security_settings']['security_captcha_send_cooldown'] = 0; $GLOBALS['aksrt_security_settings']['security_captcha_to_hour'] = 0; $GLOBALS['aksrt_security_settings']['security_captcha_to_day'] = 0; $GLOBALS['aksrt_security_settings']['security_captcha_ip_10min'] = 0; $GLOBALS['aksrt_security_settings']['security_captcha_ip_day'] = 0; for ($send = 0; $send < 100; $send++) { site_security_check(SendLimit::record('user@example.test') === 0, 'disabled send caps should not cooldown'); } site_security_check(SendLimit::check('user@example.test')['ok'] === true, 'disabled send caps still blocked sends'); $GLOBALS['aksrt_security_settings'] = []; // 5) Runtime defaults mirror the schema defaults. $security = ExtensionRuntime::get_security_settings(); site_security_check($security['security_brush_post_10min'] === 20 && $security['security_brush_post_day'] === 50, 'post cap defaults are wrong'); site_security_check($security['security_brush_comment_10min'] === 20 && $security['security_brush_comment_day'] === 100, 'comment cap defaults are wrong'); site_security_check($security['security_brush_upload_10min'] === 50 && $security['security_brush_upload_day'] === 200, 'upload cap defaults are wrong'); site_security_check($security['security_brush_register_10min'] === 20 && $security['security_brush_register_day'] === 100, 'register cap defaults are wrong'); site_security_check($security['security_brush_order_10min'] === 30 && $security['security_brush_order_day'] === 100, 'order cap defaults are wrong'); site_security_check($security['security_captcha_send_cooldown'] === 60, 'send cooldown default is wrong'); site_security_check($security['security_captcha_to_hour'] === 15 && $security['security_captcha_to_day'] === 30, 'recipient cap defaults are wrong'); site_security_check($security['security_captcha_ip_10min'] === 20 && $security['security_captcha_ip_day'] === 50, 'IP cap defaults are wrong'); site_security_check($security['security_error_frequency_limit'] === true, 'error frequency default is wrong'); site_security_check($security['security_disabled_pingback'] === true && $security['security_disabled_xmlrpc'] === true, 'hardening switch defaults are wrong'); site_security_check($security['security_user_disable_admin'] === true && $security['close_attachment_page'] === true, 'access switch defaults are wrong'); $GLOBALS['aksrt_security_settings']['security_brush_post_10min'] = '35'; $GLOBALS['aksrt_security_settings']['close_attachment_page'] = 0; $stored = ExtensionRuntime::get_security_settings(); site_security_check($stored['security_brush_post_10min'] === 35, 'stored cap override was not honored'); site_security_check($stored['close_attachment_page'] === false, 'stored switch override was not honored'); $GLOBALS['aksrt_security_settings'] = []; // 6) Schema: security section is registered, defaults match, and no 人机验证 fields exist. $extensions = SchemaExtensions::build(); site_security_check(array_key_exists('security', $extensions['sections']), 'security section is not registered under extensions'); $security_schema = $extensions['sections']['security']; $group_keys = array_column($security_schema['groups'], 'key'); foreach (['brush_post', 'brush_comment', 'brush_upload', 'brush_register', 'brush_order', 'brush_captcha', 'harden'] as $expected_group) { site_security_check(in_array($expected_group, $group_keys, true), 'missing security group: ' . $expected_group); } $fields = []; foreach ($security_schema['groups'] as $group) { foreach ($group['fields'] as $field) { $fields[$field['key']] = $field; } } $expected_fields = [ 'security_brush_post_10min' => 20, 'security_brush_post_day' => 50, 'security_brush_comment_10min' => 20, 'security_brush_comment_day' => 100, 'security_brush_upload_10min' => 50, 'security_brush_upload_day' => 200, 'security_brush_register_10min' => 20, 'security_brush_register_day' => 100, 'security_brush_order_10min' => 30, 'security_brush_order_day' => 100, 'security_captcha_send_cooldown' => 60, 'security_captcha_to_hour' => 15, 'security_captcha_to_day' => 30, 'security_captcha_ip_10min' => 20, 'security_captcha_ip_day' => 50, ]; foreach ($expected_fields as $key => $default) { site_security_check(isset($fields[$key]), 'missing security field: ' . $key); site_security_check((int) $fields[$key]['default'] === $default, 'wrong default for ' . $key); site_security_check((int) $fields[$key]['min'] === 0, 'missing zero=unlimited min for ' . $key); } foreach (['security_error_frequency_limit', 'security_disabled_pingback', 'security_disabled_xmlrpc', 'security_user_disable_admin', 'close_attachment_page'] as $switch_key) { site_security_check(isset($fields[$switch_key]) && $fields[$switch_key]['type'] === 'switch', 'missing hardening switch: ' . $switch_key); site_security_check(!empty($fields[$switch_key]['default']), 'hardening switch should default to on: ' . $switch_key); } foreach (array_keys($fields) as $field_key) { site_security_check(!str_starts_with($field_key, 'verification_'), '人机验证 field leaked into security schema: ' . $field_key); site_security_check(!str_contains($field_key, 'geetest') && !str_contains($field_key, 'tcaptcha'), 'provider captcha field leaked into security schema: ' . $field_key); } site_security_check(isset($fields['close_attachment_page']), 'close_attachment_page was not moved into the security schema'); echo "Site security harness passed\n"; }