From 6d4349ff7bcfad89f144f3805eab92c1e9113dab Mon Sep 17 00:00:00 2001 From: Malin Date: Thu, 9 Apr 2026 11:45:26 +0200 Subject: [PATCH] feat: initial InformatiQ Toolkit plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into a single unified plugin with the following improvements: - Fixed deactivation bug: all protection methods now guard themselves with their own option check so toggling off via AJAX takes effect immediately without any hook re-registration. - Added rate-limiting for good/legitimate bots (Googlebot, Bingbot, DuckDuckBot, Yandex, etc.) via transient sliding-window counters; configurable per-bot limits in goodbots.conf (BotName|req/min); returns HTTP 429 with Retry-After: 60 when over limit. - Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables) replaces the old wp_options-based 100-entry cap. - New Dashboard tab with terminal-style bot activity monitor: total blocked, today's count, rate-limited hits, top threat sources (bar chart), top IPs, top honeypot form types, active-module status panel. - All optimizations from utils.php merged into Optimization tab as toggleable settings (was always-on before). - Single admin page (Settings → InformatiQ Toolkit) with 8 tabs: Dashboard | Bot Blocker | Protection | Optimization | Honeypot | Bot Logs | Honeypot Logs | Config Files. - Config file editor for badbots.conf, goodbots.conf, referrers.conf, networks.conf, allowed-ips.conf with AJAX save and transient flush. Co-Authored-By: Claude Sonnet 4.6 --- assets/css/admin.css | 310 ++++++++++ assets/js/admin.js | 79 +++ config/allowed-ips.conf | 6 + config/badbots.conf | 406 +++++++++++++ config/goodbots.conf | 23 + config/networks.conf | 254 ++++++++ config/payloads.conf | 66 +++ config/referrers.conf | 35 ++ includes/class-itk-admin.php | 862 ++++++++++++++++++++++++++++ includes/class-itk-bot-blocker.php | 305 ++++++++++ includes/class-itk-database.php | 283 +++++++++ includes/class-itk-honeypot.php | 277 +++++++++ includes/class-itk-optimization.php | 359 ++++++++++++ includes/class-itk-protection.php | 306 ++++++++++ index.php | 1 + informatiq-toolkit.php | 147 +++++ uninstall.php | 20 + 17 files changed, 3739 insertions(+) create mode 100644 assets/css/admin.css create mode 100644 assets/js/admin.js create mode 100644 config/allowed-ips.conf create mode 100644 config/badbots.conf create mode 100644 config/goodbots.conf create mode 100644 config/networks.conf create mode 100644 config/payloads.conf create mode 100644 config/referrers.conf create mode 100644 includes/class-itk-admin.php create mode 100644 includes/class-itk-bot-blocker.php create mode 100644 includes/class-itk-database.php create mode 100644 includes/class-itk-honeypot.php create mode 100644 includes/class-itk-optimization.php create mode 100644 includes/class-itk-protection.php create mode 100644 index.php create mode 100644 informatiq-toolkit.php create mode 100644 uninstall.php diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..390c707 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,310 @@ +/* ============================================================ + InformatiQ Toolkit – Admin CSS + ============================================================ */ + +/* ── Page wrapper ─────────────────────────────────────────── */ +.itk-wrap { max-width: 1400px; } + +.itk-page-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 22px; + margin: 20px 0 16px; +} +.itk-logo { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; height: 36px; + background: #1a4a8a; + color: #fff; + font-weight: 900; + font-size: 14px; + border-radius: 6px; + letter-spacing: -1px; +} + +/* ── Tab navigation ───────────────────────────────────────── */ +.itk-tabs { + display: flex; + flex-wrap: wrap; + gap: 2px; + border-bottom: 2px solid #c3c4c7; + margin-bottom: 20px; +} +.itk-tab { + padding: 8px 16px; + text-decoration: none; + color: #2c3338; + font-size: 13px; + border-radius: 4px 4px 0 0; + border: 1px solid transparent; + border-bottom: none; + background: #f0f0f1; + margin-bottom: -2px; +} +.itk-tab:hover { background: #fff; color: #1a4a8a; } +.itk-tab-active { + background: #fff; + border-color: #c3c4c7; + color: #1a4a8a; + font-weight: 600; +} + +/* ── Settings grid ────────────────────────────────────────── */ +.itk-settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 20px; + align-items: start; +} +.itk-card { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 6px; + padding: 20px 24px; +} +.itk-card h2 { + margin: 0 0 16px; + font-size: 15px; + color: #1a4a8a; + border-bottom: 1px solid #e5e5e5; + padding-bottom: 8px; +} + +/* ── Toggle rows ──────────────────────────────────────────── */ +.itk-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid #f0f0f1; + gap: 12px; +} +.itk-toggle-row:last-child { border-bottom: none; } +.itk-toggle-info { flex: 1; } +.itk-toggle-label { display: block; font-weight: 600; font-size: 13px; color: #2c3338; } +.itk-toggle-desc { display: block; font-size: 11px; color: #646970; margin-top: 2px; } + +/* ── Toggle switch ────────────────────────────────────────── */ +.itk-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; +} +.itk-switch input { opacity: 0; width: 0; height: 0; } +.itk-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #ccc; + border-radius: 24px; + transition: .25s; +} +.itk-slider:before { + content: ''; + position: absolute; + width: 18px; height: 18px; + left: 3px; bottom: 3px; + background: #fff; + border-radius: 50%; + transition: .25s; +} +input:checked + .itk-slider { background: #2271b1; } +input:checked + .itk-slider:before { transform: translateX(20px); } + +/* ── Dashboard ────────────────────────────────────────────── */ +.itk-dashboard { + display: grid; + grid-template-columns: 1fr 280px; + gap: 20px; + align-items: start; +} +@media (max-width: 960px) { .itk-dashboard { grid-template-columns: 1fr; } } + +/* Monitor panel – terminal style */ +.itk-monitor-panel { + background: #0d1117; + color: #58a6ff; + border-radius: 8px; + padding: 20px 24px; + font-family: 'Courier New', Courier, monospace; +} +.itk-monitor-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #21262d; + padding-bottom: 12px; + margin-bottom: 16px; +} +.itk-monitor-title { font-weight: 700; font-size: 13px; letter-spacing: 1px; color: #79c0ff; } +.itk-monitor-blink { + background: #238636; + color: #fff; + padding: 2px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; + animation: itk-pulse 2s infinite; +} +@keyframes itk-pulse { 0%,100%{opacity:1} 50%{opacity:.5} } + +/* Stat cards (inside monitor) */ +.itk-stat-row { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 20px; +} +.itk-stat-card { + background: #161b22; + border: 1px solid #21262d; + border-radius: 6px; + padding: 12px 18px; + min-width: 110px; + text-align: center; + flex: 1; +} +.itk-stat-num { font-size: 2em; font-weight: 700; color: #58a6ff; line-height: 1.2; } +.itk-stat-lbl { font-size: 10px; color: #8b949e; letter-spacing: .5px; text-transform: uppercase; } +.itk-green { color: #3fb950; } +.itk-yellow { color: #d29922; } + +/* Chart sections */ +.itk-chart-section { margin-bottom: 20px; } +.itk-chart-title { + font-size: 10px; + letter-spacing: 1px; + color: #8b949e; + text-transform: uppercase; + margin-bottom: 8px; +} +.itk-bar-chart { display: flex; flex-direction: column; gap: 6px; } +.itk-bar-row { display: flex; align-items: center; gap: 8px; font-size: 12px; } +.itk-bar-label { width: 130px; color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.itk-bar-track { flex: 1; background: #21262d; border-radius: 3px; height: 14px; overflow: hidden; } +.itk-bar-fill { height: 100%; background: linear-gradient(90deg, #1f6feb, #58a6ff); border-radius: 3px; transition: width .4s; } +.itk-bar-hp { background: linear-gradient(90deg, #6e40c9, #a371f7) !important; } +.itk-bar-count { width: 40px; text-align: right; color: #8b949e; font-size: 11px; } + +/* Mini table (top IPs) */ +.itk-mini-table { width: 100%; border-collapse: collapse; font-size: 12px; } +.itk-mini-table th { color: #8b949e; font-weight: normal; padding: 4px 8px; border-bottom: 1px solid #21262d; } +.itk-mini-table td { color: #c9d1d9; padding: 4px 8px; border-bottom: 1px solid #161b22; } +.itk-mini-table a { color: #58a6ff; } +.itk-lookup { color: #8b949e; font-size: 10px; } + +/* Quick status sidebar */ +.itk-quick-status { + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 6px; + padding: 16px 20px; +} +.itk-quick-status .itk-chart-title { color: #1a4a8a; } +.itk-module-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #f0f0f1; + font-size: 12px; +} +.itk-module-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.itk-dot-on { background: #00a32a; } +.itk-dot-off { background: #c3c4c7; } +.itk-module-label { flex: 1; color: #2c3338; } +.itk-module-status { font-size: 10px; font-weight: 700; letter-spacing: .5px; } +.itk-dot-on ~ .itk-module-status { color: #00a32a; } +.itk-dot-off ~ .itk-module-status { color: #c3c4c7; } + +/* ── Log tables ───────────────────────────────────────────── */ +.itk-log-page {} +.itk-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-bottom: 12px; +} +.itk-filters input[type=text], +.itk-filters select { height: 32px; padding: 0 8px; } +.itk-count { color: #646970; font-size: 13px; margin: 0 0 8px; } +.itk-log-table { font-size: 12px; } +.itk-log-table th { white-space: nowrap; } +.itk-nowrap { white-space: nowrap; } +.itk-ua { color: #646970; font-size: 11px; max-width: 220px; word-break: break-all; } +.itk-uri { font-size: 11px; color: #646970; max-width: 200px; word-break: break-all; } +.itk-no-results { text-align: center; padding: 20px; color: #646970; } +.itk-filter-link { font-size: 10px; color: #2271b1; text-decoration: none; } +.itk-filter-link:hover { text-decoration: underline; } + +/* Badges */ +.itk-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 3px; + font-size: 10px; + font-weight: 700; + letter-spacing: .5px; + text-transform: uppercase; +} +.itk-badge-block { background: #ffecec; color: #b32d2e; border: 1px solid #f7c5c5; } +.itk-badge-warn { background: #fef3cd; color: #856404; border: 1px solid #fde68a; } +.itk-badge-hp { background: #f3e8ff; color: #6e40c9; border: 1px solid #d4a9ff; } + +/* Pager */ +.itk-pager { + display: flex; + align-items: center; + gap: 8px; + margin: 12px 0; + font-size: 13px; +} +.itk-pager span { color: #646970; } + +/* Danger button */ +.itk-btn-danger { color: #b32d2e !important; border-color: #b32d2e !important; } +.itk-btn-danger:hover { background: #b32d2e !important; color: #fff !important; } + +/* ── Config editor ────────────────────────────────────────── */ +.itk-config-editor {} +.itk-config-tabs { + display: flex; + gap: 4px; + margin-bottom: 16px; + flex-wrap: wrap; +} +.itk-config-tab { + padding: 6px 14px; + background: #f0f0f1; + border: 1px solid #c3c4c7; + border-radius: 4px; + text-decoration: none; + color: #2c3338; + font-size: 12px; +} +.itk-config-tab.active { background: #2271b1; color: #fff; border-color: #2271b1; } +.itk-config-editor-area { background: #fff; border: 1px solid #c3c4c7; border-radius: 6px; padding: 20px; } +.itk-config-textarea { + width: 100%; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + background: #0d1117; + color: #c9d1d9; + border: 1px solid #30363d; + border-radius: 4px; + padding: 12px; + resize: vertical; + box-sizing: border-box; +} + +/* ── Toggle feedback ──────────────────────────────────────── */ +.itk-toggle-saving { opacity: .6; pointer-events: none; } +.itk-toggle-saved { color: #00a32a; font-size: 11px; } +.itk-toggle-error { color: #b32d2e; font-size: 11px; } diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..2baa6dd --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,79 @@ +/* InformatiQ Toolkit – Admin JS */ +(function ($) { + 'use strict'; + + /* ── Toggle switches (AJAX) ───────────────────────────────── */ + $(document).on('change', '.itk-toggle-input', function () { + var $input = $(this); + var $row = $input.closest('.itk-toggle-row'); + var option = $input.data('option'); + var setting = $input.data('setting'); + var value = $input.is(':checked') ? 1 : 0; + + $row.addClass('itk-toggle-saving'); + + $.post(itkAdmin.ajaxUrl, { + action: 'itk_save_setting', + nonce: itkAdmin.nonce, + option: option, + setting: setting, + value: value + }) + .done(function (res) { + if (res.success) { + showFeedback($row, 'itk-toggle-saved', 'Saved'); + } else { + $input.prop('checked', !$input.is(':checked')); // revert + showFeedback($row, 'itk-toggle-error', 'Error saving'); + } + }) + .fail(function () { + $input.prop('checked', !$input.is(':checked')); + showFeedback($row, 'itk-toggle-error', 'Request failed'); + }) + .always(function () { + $row.removeClass('itk-toggle-saving'); + }); + }); + + function showFeedback($row, cls, msg) { + $row.find('.itk-feedback').remove(); + var $fb = $('' + msg + ''); + $row.append($fb); + setTimeout(function () { $fb.fadeOut(400, function () { $(this).remove(); }); }, 2000); + } + + /* ── Config file editor (AJAX) ────────────────────────────── */ + $('#itk-save-config').on('click', function (e) { + e.preventDefault(); + var $btn = $(this); + var file = $btn.data('file'); + var content = $('#itk-config-content').val(); + var $status = $('#itk-config-status'); + + $btn.prop('disabled', true).text('Saving…'); + $status.hide(); + + $.post(itkAdmin.ajaxUrl, { + action: 'itk_save_config_file', + nonce: itkAdmin.nonce, + file: file, + content: content + }) + .done(function (res) { + if (res.success) { + $status.text('Saved!').css('color', '#00a32a').show(); + } else { + $status.text('Error: ' + (res.data || 'unknown')).css('color', '#b32d2e').show(); + } + }) + .fail(function () { + $status.text('Request failed.').css('color', '#b32d2e').show(); + }) + .always(function () { + $btn.prop('disabled', false).text('Save File'); + setTimeout(function () { $status.fadeOut(); }, 3000); + }); + }); + +})(jQuery); diff --git a/config/allowed-ips.conf b/config/allowed-ips.conf new file mode 100644 index 0000000..4ec75d1 --- /dev/null +++ b/config/allowed-ips.conf @@ -0,0 +1,6 @@ +194.56.239.153 +109.69.48.0 +195.154.47.0 +127.0.0.1 +192.168.0.0/24 +192.168.1.1/24 \ No newline at end of file diff --git a/config/badbots.conf b/config/badbots.conf new file mode 100644 index 0000000..9372179 --- /dev/null +++ b/config/badbots.conf @@ -0,0 +1,406 @@ +# OpenAI bots are handled separately in the plugin code + +# Common malicious bots and user agents from .htaccess +jorgee +morfeus +firefox/40.1 +firefox/34.0 +firefox/32.1 +firefox/19.0 +firefox/38.0 +firefox/18.0 +wget +curl +libwww-perl +WinHttp +okhttp +python +java +WebReaper +WebSauger +Website eXtractor +Website Quester +Webster +WebStripper +WebWhacker +WebZIP +Whacker +BatchFTP +HTTrack +Harvest +Collector +Copier +Extractor +lftp +libWeb/clsHTTP +Mirror +Net Vampire +Offline Explorer +Offline Navigator +PageGrabber +Sucker +SuperHTTP +Teleport +Vacuum +Web Sucker +WebAuto +WebBandit +Webclipping.com +WebCopier +WebEnhancer +WebFetch +WebLeacher +WWWOFFLE +WWW-Collector-E +Go-Ahead-Got-It +gotit +GrabNet +lwp-trivial +LWP::Simple +Magnet +Mag-Net +moget +MIDown tool +NetSpider +NetZIP +Reaper +Recorder +ReGet +RepoMonkey +Siphon +SiteSnagger +AppsViewer +Lynx +Acunetix +FHscan +Baidu +Yandex +Download Demon +Download Devil +Download Wonder +EirGrabber +EasyDL +Mass Downloader +RealDownload +SmartDownload +EmailCollector +EmailSiphon +EmailWolf +WebEMailExtrac +EmailSiphon +Mail +slurp +MJ12 +FastProbe +spbot +dotbot +semrush +Daum +duckduckgo +teoma +Aboundex +80legs +360Spider +Cogentbot +Alexibot +asterias +attach +BackDoorBot +BackWeb +Bandit +Bigfoot +Black.Hole +BlackWidow +BlowFish +BotALot +Buddy +BuiltBotTough +Bullseye +BunnySlippers +Cegbfeieh +CheeseBot +CherryPicker +ChinaClaw +CopyRightCheck +cosmos +Crescent +Custo +AIBOT +DISCo +DIIbot +DittoSpyder +dragonfly +Drip +eCatch +ebingbong +EroCrawler +EyeNetIE +Foobot +flunky +FrontPage +Grafula +hloader +HMView +humanlinks +IlseBot +Indy Library +InfoNaviRobot +InfoTekies +Intelliseek +InterGET +Internet Ninja +Iria +Jakarta +JennyBot +JetCar +JOC +JustView +Jyxobot +Kenjin.Spider +Keyword.Density +larbin +LexiBot +likse +MarkWatch +Mata.Hari +Memo +Microsoft.URL +Microsoft URL Control +MIIxpc +Missigua Locator +Mister PiX +NAMEPROTECT +Navroad +NearSite +NetAnts +Netcraft +NetMechanic +NextGenSearchBot +NICErsPRO +niki-bot +NimbleCrawler +Ninja +NPbot +Octopus +Openfind +OutfoxBot +Papa Foto +pavuk +pcBrowser +PHP version tracker +Pockey +ProPowerBot/2.14 +ProWebWalker +psbot +Pump +QueryN.Metasearch +SlySearch +Snake +Snapbot +Snoopy +sogou +SpaceBison +SpankBot +spanner +Sqworm +Stripper +SuperBot +Surfbot +suzuran +Szukacz/1.4 +tAkeOut +Telesoft +TurnitinBot/1.5 +The.Intraformant +TheNomad +TightTwatBot +Titan +True_Robot +turingos +TurnitinBot +URLy.Warning +VCI +VoidEYE +WebmasterWorldForumBot +WebGo IS +Widow +WISENutbot +Xaldon +Zeus +ZmEu +Zyborg +crawle +igdeSpyder +Robot +Aport +spider +Parser +ahref +zoom +Powermarks +SafeDNS +BLEXBot +aria2 +wikido +Qwantify +DotBot +FatBot +grapeshot +Nutch +linkdexbot +Twitterbot +Google-HTTP-Java-Client +MetaCommentBot +Veoozbot +ScoutJet +DomainAppender +Windows 2005 +Go-http-client +Drupal +OrangeBot +CCBot +WBSearchBot +SEOkicks +WHR +sqlmap +ltx71 +aiHitBot +InfoPath +Superfeedr +rogerbot +Alltop +heritrix +indiensolidaritet +Experibot +magpie +RSSInclude +wp-android +XML-RPC.NET +Synapse +GimmeUSAbot +istellabot +interfax +vebidoobot +oBot +Jetty +mozilla16.2.exe +dataaccessd +(compatible;) +Dalvik +eCairn +istellabot +InetURL +BazQux +Wotbox +null +scrapy-redis +weborama-fetcher +TrapitAgent +UNKNOWN +SeznamBot +Dataprovider +msnbot-Products +masscan +istellabot +BUbiNG +.NET +cliqzbot +Deepnet +Ziba +SMTBot +MojeekBot +linqia +portscout +Dataprovider +ia_archiver +Dalvik +MEGAsync +GroupHigh +Moreover +YisouSpider +YahooCacheSystem +Clickagy +Go-http-client +SMUrlExpander +XoviBot +MSIE3.00 +MSIE2.00 +MSIE4.00 +MSIECrawler +Windows 2005 +Windows 2008 +Windows 2004 +Windows 2003 +Windows 2002 +XoviBot +Qwantify +BOT for JCE +Jorgee +YaK +iTunes +Mechanize +Mail.RU_Bot +zgrab +Owler +Barkrowler +SearchmetricsBot +extlinks +archive-it +BDCbot +SuperPagesUrlVerifyBot +Siteimprove +Freshbot +WebDAV +ips-agent +PiplBot +coccocbot-web +Alexa Toolbar +scrapinghub +Twingly +sysscan +trendictionbot0 +DnyzBot +rogerbot +GridBot +DnyzBot +PiplBot +BoardReader +SafeDNSBot +Insideview +coccocbot +PolycomVVX +^Mozilla/5.0$ +^The Knowledge AI +SputnikBot +od-database-crawler +Hype%20Machine +The Hype Machine Engine +Apache-HttpClient +Goodzer +Knowledge +Linguee +serpstatbot +PHP/5 +PHP/4 +PHP/3 +Thumbtack-Thunderdome +Googlebot-Image +Googlebot-Video +bingpreview +msnbot-media +Exabot +Image Stripper +Image Sucker +Express WebPictures +Web Image Collector +Web.Image.Collector +YandexImages +Firefox mutant +Ukraine Local +Mozilla/3.Mozilla/2.01 +Mozilla.*NEWT +LinkextractorPro +LinkScan/8.1a.Unix +LNSpiderguy +LinkWalker +Xenu \ No newline at end of file diff --git a/config/goodbots.conf b/config/goodbots.conf new file mode 100644 index 0000000..677310f --- /dev/null +++ b/config/goodbots.conf @@ -0,0 +1,23 @@ +# Good/Legitimate bots - these are rate-limited but NOT blocked +# Format: BotName|rate_per_minute +# Lines starting with # are comments + +Googlebot|60 +Bingbot|60 +DuckDuckBot|60 +Baiduspider|30 +YandexBot|30 +Sogou|20 +Applebot|30 +facebot|30 +ia_archiver|20 +Twitterbot|30 +LinkedInBot|30 +Slurp|30 +MJ12bot|20 +AhrefsBot|10 +SemrushBot|10 +DotBot|20 +PetalBot|20 +Bytespider|20 +GPTBot|0 diff --git a/config/networks.conf b/config/networks.conf new file mode 100644 index 0000000..be750e6 --- /dev/null +++ b/config/networks.conf @@ -0,0 +1,254 @@ +# IP addresses and networks to block extracted from .htaccess + +# Aliyun +121.40.0.0/14 +121.40.0.0/15 + +# Cyveillance subnets +38.100.19.8/29 +38.100.21.0/24 +38.100.41.64/26 +38.105.71.0/25 +38.105.83.0/27 +38.112.21.140/30 +38.118.42.32/29 +65.213.208.128/27 +65.222.176.96/27 +65.222.185.72/29 + +# Poneytelecom subnets +62.4.0.0/19 +62.210.0.0/16 +195.154.0.0/16 +212.47.224.0/19 +212.83.128.0/19 +212.83.160.0/19 +212.129.0.0/18 + +# Ecatel & Leaseweb subnets +80.82.64.0/24 +80.82.65.0/24 +80.82.66.0/24 +80.82.67.0/24 +80.82.68.0/24 +80.82.69.0/24 +80.82.70.0/24 +80.82.76.0/24 +80.82.77.0/24 +80.82.78.0/24 +80.82.79.0/24 +89.248.160.0/21 +89.248.168.0/24 +89.248.169.0/24 +89.248.170.0/23 +89.248.172.0/23 +89.248.174.0/24 +93.174.88.0/21 +94.102.48.0/20 +188.72.106.0/24 +188.72.117.0/24 +185.56.80.125 + +# Aboundex +173.192.34.95 + +# Bluecoat +8.21.4.254 +65.46.48.192/30 +65.160.238.176/28 +85.92.222.0/24 +206.51.36.0/22 +216.52.23.0/24 + +# Cyberpatrol +38.103.17.160/27 + +# Internet Identity - Anti-Phishing +66.113.96.0/20 +70.35.113.192/27 + +# Ironport +204.15.80.0/22 + +# Lightspeed Systems Security +66.17.15.128/26 +69.84.207.32/27 +69.84.207.128/25 + +# Layered Technologies +72.36.128.0/17 +72.232.0.0/16 +72.233.0.0/17 +216.32.0.0/14 + +# M86 +67.192.231.224/29 +208.90.236.0/22 + +# Phish-Inspector.com +209.147.127.208/28 + +# Prescient Software, Inc. Phishmongers +198.186.190.0/23 +198.186.192.0/23 +198.186.194.0/24 + +# urlfilterdb +207.210.99.32/29 + +# websense-in.car1.sandiego1.level3.net +4.53.120.22 + +# Websense +66.194.6.0/24 +67.117.201.128/28 +69.67.32.0/20 +131.191.87.0/24 +204.15.64.0/21 +208.80.192.0/21 +212.62.26.64/27 +213.168.226.0/24 +213.168.241.0/30 +213.168.242.0/30 +213.236.150.16/28 + +# IP Strada & co. +162.211.104.0/22 +162.218.56.0/21 +198.89.232.0/21 +199.15.232.0/21 +199.15.232.0/24 +199.15.233.0/24 +199.15.234.0/24 +199.15.235.0/24 +199.15.237.0/24 +199.15.238.0/24 +199.15.239.0/24 + +# DigitalOcean +45.55.100.0/22 +45.55.116.0/22 +67.207.66.0/24 +104.131.192.0/19 +104.131.224.0/19 +107.170.0.0/17 +107.170.128.0/19 +107.170.160.0/19 +138.197.240.0/22 +138.197.252.0/22 +159.203.152.0/22 +162.243.0.0/17 +162.243.191.0/24 +162.243.192.0/18 +192.241.160.0/19 +192.241.240.0/20 + +# vHoster Ukraine doing WP bruteforce +91.200.12.0/22 + +# Drake Holdings +192.92.196.0/24 +204.79.180.0/24 + +# Hetzner Denies +193.47.99.0/24 +188.40.0.0/16 +185.12.64.0/22 +178.63.0.0/16 +176.9.0.0/16 +213.239.192.0/18 +213.133.96.0/19 +88.198.0.0/16 +85.10.192.0/18 +78.46.0.0/15 +5.9.0.0/17 +5.9.0.0/16 +46.4.0.0/16 +88.99.0.0/16 +91.220.49.0/24 +91.233.8.0/22 +94.130.0.0/16 +95.216.0.0/16 +95.217.0.0/16 +136.243.0.0/16 +138.201.0.0/16 +144.76.0.0/16 +148.251.0.0/16 +176.102.168.0/21 +185.50.120.0/23 +185.107.52.0/22 +185.126.28.0/22 +185.136.140.0/23 +185.141.200.0/24 +185.141.202.0/24 +185.171.224.0/22 +185.185.26.0/23 +185.189.228.0/24 +185.189.230.0/24 +185.189.231.0/24 +185.209.124.0/22 +185.216.237.0/24 +185.228.8.0/22 +193.25.170.0/23 +193.110.6.0/23 +193.223.77.0/24 +194.42.180.0/22 +194.42.184.0/22 +194.145.226.0/24 +195.60.226.0/24 +195.248.224.0/24 +197.242.84.0/22 + +# Seznam bot +77.75.72.0/23 +77.75.74.0/24 +77.75.75.0/24 +77.75.76.0/23 +77.75.78.0/23 +185.66.188.0/22 + +# Quasi Networks - Spammers +145.249.104.0/22 +185.216.140.0/23 +188.72.103.0/24 +188.72.106.0/24 +188.72.117.0/24 +196.16.0.0/14 +213.184.105.0/24 +213.184.113.0/24 +213.184.115.0/24 +213.184.117.0/24 + +# DataShack / Wholesale Internet / VPN Consumer Network / My Server Planet / VoIP DediNet & co. +63.141.224.0/19 +69.30.192.0/24 +69.30.204.0/24 +69.30.220.0/24 +69.30.228.0/24 +69.30.235.0/24 +69.30.237.0/24 +69.197.148.0/24 +69.197.152.0/24 +69.197.170.0/24 +69.197.171.0/24 +69.197.173.0/24 +69.197.178.0/24 +74.91.16.0/20 +104.37.30.0/24 +107.150.32.0/19 +142.54.160.0/19 +173.46.91.0/24 +173.46.93.0/24 +192.151.144.0/20 +192.187.96.0/19 +198.204.224.0/19 +199.168.96.0/21 +204.12.199.0/24 +204.12.200.0/24 +204.12.203.0/24 +204.12.205.0/24 +204.12.245.0/24 +208.67.0.0/24 +208.67.1.0/24 +208.110.85.0/24 +208.110.87.0/24 \ No newline at end of file diff --git a/config/payloads.conf b/config/payloads.conf new file mode 100644 index 0000000..b519835 --- /dev/null +++ b/config/payloads.conf @@ -0,0 +1,66 @@ +# Known attack payload regex patterns +# One pattern per line, these are checked against request parameters and user input +# Lines starting with # are comments + +# XSS attack patterns +# Pattern for alert/prompt/confirm execution +/(?:<|%3C|<)(?:script|iframe|svg|img|a).*?(?:alert|prompt|confirm|eval)\s*\(.*?\)/i +# Pattern for script injection +/(?:<|%3C|<)script.*?(?:>|%3E|>)/i +# Pattern for event handlers like onerror, onload, etc. +/\bon(?:error|load|click|mouseover|focus|blur)\s*=\s*["']?(?:alert|prompt|confirm|eval)/i +# Pattern for javascript: protocol +/javascript\s*:\s*(?:alert|prompt|confirm|eval)/i +# Pattern for data URI scheme with script +/data\s*:\s*(?:text|application)\/(?:javascript|html).*?base64/i + +# SQL Injection patterns +# Pattern for basic SQL injection attempts +/(?:'\s*OR\s*'[\w\d]+'?\s*=\s*'[\w\d]+)|(?:"\s*OR\s*"[\w\d]+"?\s*=\s*"[\w\d]+")/i +# Pattern for SQL comments +/(?:--|#|\/\*)[^\w\d]*(?:union|select|insert|update|delete|drop|alter)/i +# Pattern for UNION SELECT attempts +/union\s+(?:all\s+)?select/i +# Pattern for SQL batch commands +/;\s*(?:drop|alter|create|truncate|rename|insert|update|delete)/i + +# Remote file inclusion patterns +# Pattern for external URL inclusion +/(?:https?|ftp|php|data|file):\/\/[^\s\n"')>]+/i +# Pattern for directory traversal +/(?:\.\.\/|\.\.\\|\.\.\%2f|\.\.\%5c)[^\s\n"')>]+/i +# Pattern for PHP wrapper usage +/php:\/\/(?:filter|input|memory|output|temp)/i + +# Command injection patterns +# Pattern for shell command execution +/[;&|`]\s*(?:ls|cat|cd|pwd|echo|rm|cp|mv|sudo|chmod|chown|wget|curl)/i +# Pattern for command substitution +/\$\([^\)]*\)|`[^`]*`/i +# Pattern for direct system command injection +/system\s*\(|exec\s*\(|shell_exec\s*\(|passthru\s*\(|eval\s*\(/i + +# Local file inclusion patterns +# Pattern for path traversal +/(?:\/|\\|\.\.|%2f|%5c)(?:etc|bin|usr|home|var|root|windows|system32)/i +# Pattern for sensitive file access +/(?:\/|\\|\.\.|%2f|%5c)(?:passwd|shadow|hosts|config|wp-config|web\.config)/i + +# XML/XXE injection patterns +/?]{10,}/ \ No newline at end of file diff --git a/config/referrers.conf b/config/referrers.conf new file mode 100644 index 0000000..4ee732f --- /dev/null +++ b/config/referrers.conf @@ -0,0 +1,35 @@ +# Known spam or malicious referrers from .htaccess +free-social-buttions.com +best-seo-offer.com +buttons-for-your-website.com +www1.free-social-buttons.com +www2.free-social-buttons.com +www3.free-social-buttons.com +100dollars-seo.com.com +anonymizeme.pro +site.ru +www4.free-social-buttons.com +free-social-buttons.com +buttons-for-website.com +social-buttons.com +anticrawler.org +blackhatworth.com +best-seo-offer.com +buttons-for-your-website.com +best-seo-solution.com +adcash.com +darodar.com +priceg.com +hulfingtonpost.com +gobongo.info +slftsdybbg.ru +ilovevitaly.com +ilovevitaly.co +ilovevitaly.ru +webmonetizer.net +make-money-online.com +cenoval.ru +o-o-6-o-o.com +7makemoneyonline.com +semalt.com +keywords-monitoring-success.com \ No newline at end of file diff --git a/includes/class-itk-admin.php b/includes/class-itk-admin.php new file mode 100644 index 0000000..f43bcc4 --- /dev/null +++ b/includes/class-itk-admin.php @@ -0,0 +1,862 @@ + wp_create_nonce(self::NONCE_ACTION), + 'ajaxUrl' => admin_url('admin-ajax.php'), + ]); + } + + /* ── Actions (form submissions) ───────────────────────────── */ + + public function handle_actions(): void { + if (!isset($_POST['itk_action']) || !check_admin_referer(self::NONCE_ACTION)) return; + if (!current_user_can('manage_options')) wp_die('Unauthorized'); + + switch ($_POST['itk_action']) { + case 'clear_bot_log': + ITK_Database::clear_bot_log(); + $this->redirect(['tab' => 'bot-logs', 'cleared' => 1]); + break; + case 'clear_honeypot_log': + ITK_Database::clear_honeypot_log(); + $this->redirect(['tab' => 'honeypot-logs', 'cleared' => 1]); + break; + case 'save_settings_security': + $this->save_settings_form('itk_security', [ + 'response_code', 'redirect_url', 'custom_message', + ], [ + 'log_blocked_attempts', + ]); + $this->redirect(['tab' => 'bot-blocker', 'saved' => 1]); + break; + case 'save_settings_login': + $this->save_settings_form('itk_security', [ + 'custom_login_slug', + ], [ + 'enable_custom_login', + ]); + $this->redirect(['tab' => 'protection', 'saved' => 1]); + break; + case 'save_settings_honeypot': + $this->save_settings_form('itk_honeypot', [ + 'min_time', 'max_time', 'retain_days', + ], []); + $this->redirect(['tab' => 'honeypot', 'saved' => 1]); + break; + } + } + + /** + * Save a subset of fields from $_POST['itk_*'] into a WP option. + * $text_fields = scalar fields (sanitize_text_field) + * $toggle_fields = checkbox fields (0 or 1) + */ + private function save_settings_form(string $option, array $text_fields, array $toggle_fields): void { + $opts = get_option($option, []); + $posted = $_POST[$option] ?? []; + + foreach ($text_fields as $key) { + if (isset($posted[$key])) { + $opts[$key] = sanitize_text_field(wp_unslash($posted[$key])); + } + } + foreach ($toggle_fields as $key) { + $opts[$key] = !empty($posted[$key]) ? 1 : 0; + } + update_option($option, $opts); + } + + private function redirect(array $args): void { + wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php'))); + exit; + } + + /* ── AJAX: save single toggle setting ─────────────────────── */ + + public function ajax_save_setting(): void { + check_ajax_referer(self::NONCE_ACTION, 'nonce'); + if (!current_user_can('manage_options')) wp_send_json_error('unauthorized'); + + $option = sanitize_key($_POST['option'] ?? ''); + $setting = sanitize_key($_POST['setting'] ?? ''); + $value = (int)($_POST['value'] ?? 0); + + $allowed = ['itk_security','itk_optimization','itk_honeypot']; + if (!in_array($option, $allowed, true) || empty($setting)) { + wp_send_json_error('invalid'); + } + + $opts = get_option($option, []); + $opts[$setting] = $value; + update_option($option, $opts); + + wp_send_json_success(); + } + + /* ── AJAX: save config file ───────────────────────────────── */ + + public function ajax_save_config_file(): void { + check_ajax_referer(self::NONCE_ACTION, 'nonce'); + if (!current_user_can('manage_options')) wp_send_json_error('unauthorized'); + + $file = sanitize_key($_POST['file'] ?? ''); + $content = wp_unslash($_POST['content'] ?? ''); + + $allowed = [ + 'badbots' => ITK_PATH . 'config/badbots.conf', + 'goodbots' => ITK_PATH . 'config/goodbots.conf', + 'referrers' => ITK_PATH . 'config/referrers.conf', + 'networks' => ITK_PATH . 'config/networks.conf', + 'allowed-ips'=> ITK_PATH . 'config/allowed-ips.conf', + ]; + + if (!isset($allowed[$file])) wp_send_json_error('invalid file'); + + $result = file_put_contents($allowed[$file], sanitize_textarea_field($content)); + if ($result === false) { + wp_send_json_error('write failed'); + } + + // Clear transient caches + delete_transient('itk_bots_list'); + delete_transient('itk_referrers_list'); + delete_transient('itk_networks_list'); + delete_transient('itk_goodbots_list'); + + wp_send_json_success(); + } + + /* ── Main page render ─────────────────────────────────────── */ + + public function render_page(): void { + if (!current_user_can('manage_options')) return; + $tab = sanitize_key($_GET['tab'] ?? 'dashboard'); + ?> +
+

+ InformatiQ Toolkit +

+ + +

Logs cleared successfully.

+ + +

Settings saved.

+ + + + +
+ $this->tab_dashboard(), + 'bot-blocker' => $this->tab_bot_blocker(), + 'protection' => $this->tab_protection(), + 'optimization' => $this->tab_optimization(), + 'honeypot' => $this->tab_honeypot(), + 'bot-logs' => $this->tab_bot_logs(), + 'honeypot-logs' => $this->tab_honeypot_logs(), + 'config-files' => $this->tab_config_files(), + default => $this->tab_dashboard(), + }; + ?> +
+
+ +
+ + +
+
+ ■ BOT ACTIVITY MONITOR + LIVE +
+ +
+
+
+
Total Blocked
+
+
+
+
Today
+
+
+
+
Rate Limited
+
+
+
+
Honeypot Catches
+
+
+
+
Honeypot Today
+
+
+ + + +
+
TOP THREAT SOURCES
+
+ cnt); + foreach ($bot_stats['top_bot_types'] as $row): + $pct = round(($row->cnt / $max) * 100); + ?> +
+ bot_type ?: 'Unknown') ?> +
+
+
+ cnt) ?> +
+ +
+
+ + + + +
+
HONEYPOT – TOP TARGETED FORMS
+
+ cnt); + foreach ($hp_stats['top_forms'] as $row): + $pct = round(($row->cnt / $max) * 100); + ?> +
+ form_type) ?> +
+
+
+ cnt) ?> +
+ +
+
+ + + + +
+
TOP OFFENDER IPs
+ + + + + + + + +
IP AddressHits
+ ip_address) ?> +  [lookup] + cnt) ?>
+
+ +
+ + +
+
ACTIVE MODULES
+ !empty($sec['block_malicious_bots']), + 'OpenAI Block' => !empty($sec['block_openai_bots']), + 'Good Bot Rate Limit'=> !empty($sec['rate_limit_good_bots']), + 'Network Block' => !empty($sec['block_bad_networks']), + 'Login Protection' => !empty($sec['protect_wp_login']), + 'Security Headers' => !empty($sec['add_security_headers']), + 'Block XML-RPC' => !empty($sec['block_xmlrpc']), + 'Honeypot' => !empty($hp['enabled']), + 'Remove WP Version' => !empty($opt['remove_wp_version']), + 'Disable Emoji' => !empty($opt['remove_emoji']), + 'Admin Branding' => !empty($opt['admin_branding']), + ]; + foreach ($modules as $label => $active): + ?> +
+ + + +
+ +
+ +
+ +
+
+

Bot Blocking

+ ['OpenAI / GPT Bots', 'Block GPTBot, ChatGPT-User, OAI-SearchBot'], + 'block_malicious_bots' => ['Malicious Bots', 'Block bots listed in badbots.conf'], + 'block_bad_referrers' => ['Bad Referrers', 'Block requests from spam referrer domains'], + 'block_bad_networks' => ['Bad Networks', 'Block IP ranges listed in networks.conf'], + 'rate_limit_good_bots' => ['Rate-Limit Good Bots', 'Apply crawl-rate limits to Googlebot, Bingbot, etc. (configurable in goodbots.conf)'], + ]; + foreach ($toggles as $key => [$label, $desc]): + $this->render_toggle('itk_security', $key, $label, $desc, $opts); + endforeach; + ?> +
+ +
+

Response Settings

+
+ + + + + + + + + + + + + + + + + + + + +
Response Code + +
Redirect URL
Custom Message
Log Blocked Attempts + +
+ +
+
+
+ +
+
+

WordPress Protection

+ ['WP Login IP Whitelist', 'Restrict wp-login.php to IPs in allowed-ips.conf'], + 'add_security_headers' => ['Security Headers', 'Add X-Content-Type-Options, X-Frame-Options, X-XSS-Protection headers'], + 'protect_wp_includes' => ['Protect WP Core Files', 'Block direct access to wp-includes, wp-admin/includes'], + 'protect_uploads' => ['Block PHP in Uploads', 'Deny PHP file access and uploads in /wp-content/uploads/'], + 'block_xmlrpc' => ['Block XML-RPC', 'Deny all access to xmlrpc.php'], + 'block_malicious_queries' => ['Block Malicious Queries', 'Detect and block SQLi, XSS, and command injection in query strings'], + 'block_author_scans' => ['Block Author Scans', 'Redirect ?author=N requests to prevent username enumeration'], + ]; + foreach ($toggles as $key => [$label, $desc]): + $this->render_toggle('itk_security', $key, $label, $desc, $opts); + endforeach; + ?> +
+ +
+

Custom Login URL

+
+ + + + render_toggle('itk_security', 'enable_custom_login', 'Enable Custom Login URL', 'Replace /wp-login.php with a custom slug', $opts); ?> + + + + + + +
Login Slug + + +

Characters: letters, numbers, dashes only.

+
+ +
+
+
+ +
+
+

Performance & Cleanup

+ ['Remove WP Version', 'Strip WordPress version from and all enqueued assets'], + 'remove_script_versions' => ['Remove Asset Versions', 'Remove ?ver= query string from CSS/JS URLs'], + 'remove_emoji' => ['Remove Emojis', 'Disable WordPress emoji scripts and styles'], + 'deregister_wp_embed' => ['Remove WP Embed', 'Deregister the wp-embed script'], + 'remove_wp_head_noise' => ['Clean WP Head', 'Remove RSD, wlwmanifest, feed links, and adjacent post links from '], + 'limit_revisions' => ['Limit Revisions', 'Keep only 3 post revisions and autosave every 5 minutes'], + 'defer_js' => ['Defer JavaScript', 'Add defer attribute to non-critical scripts'], + 'limit_heartbeat' => ['Limit Heartbeat', 'Restrict WordPress heartbeat to post editor pages only'], + 'stop_empty_search_redirect'=> ['Fix Empty Search', 'Prevent redirect loop on empty search queries'], + 'use_google_jquery' => ['Use Google jQuery', 'Load jQuery from Google CDN instead of local'], + 'dns_prefetch' => ['DNS Prefetch', 'Enable DNS prefetching via meta header'], + ]; + foreach ($toggles as $key => [$label, $desc]): + $this->render_toggle('itk_optimization', $key, $label, $desc, $opts); + endforeach; + ?> +
+ +
+

Security Tweaks

+ ['Hide Login Errors', 'Replace specific login error messages with a generic one'], + 'remove_author_class' => ['Remove Author Class', 'Strip admin username from comment CSS classes'], + 'remove_default_userfields'=> ['Remove User Fields', 'Remove AIM, Jabber, YIM from user profiles'], + 'clean_bad_content' => ['Clean Bad Content', 'Remove empty tags, inline styles, and font tags on save'], + 'change_author_base' => ['Change Author URL Base', "Change /author/ to /writer/ in author archive URLs"], + 'disable_xml_rpc' => ['Disable XML-RPC (via filter)', 'Filter-based XML-RPC disable (additional to the blocker)'], + ]; + foreach ($toggles as $key => [$label, $desc]): + $this->render_toggle('itk_optimization', $key, $label, $desc, $opts); + endforeach; + ?> +
+ +
+

UI / Branding

+ ['Disable Core Widgets', 'Remove WordPress News and WPEngine news dashboard widgets'], + 'unregister_default_widgets'=> ['Unregister Sidebar Widgets', 'Remove Calendar, Archives, Meta, Search, Tag Cloud widgets'], + 'disable_comments_url' => ['Remove Comment URL Field', 'Hide the website URL field from comment forms'], + 'remove_admin_bar_links' => ['Clean Admin Bar', 'Remove WordPress logo, links, and noisy items from the toolbar'], + 'admin_branding' => ['InformatiQ Branding', 'Add InformatiQ logo widget, toolbar link, admin notice, and custom footer'], + 'disable_floc' => ['Disable FLoC', 'Add Permissions-Policy: interest-cohort=() header'], + 'lightbox_images' => ['Lightbox Images', 'Add rel="lightbox" to image links in post content'], + 'featured_image_rss' => ['Featured Image in RSS', 'Include featured image in RSS feed entries'], + ]; + foreach ($toggles as $key => [$label, $desc]): + $this->render_toggle('itk_optimization', $key, $label, $desc, $opts); + endforeach; + ?> +
+
+ +
+
+

Honeypot Fields

+ ['Enable Honeypot', 'Inject invisible honeypot fields into all protected forms'], + 'protect_comments' => ['Comments', 'Protect comment forms'], + 'protect_login' => ['Login Form', 'Protect wp-login.php login'], + 'protect_register' => ['Registration', 'Protect user registration'], + 'protect_lost_password' => ['Lost Password', 'Protect lost password form'], + 'protect_woocommerce' => ['WooCommerce', 'Protect WooCommerce checkout and registration'], + 'protect_cf7' => ['Contact Form 7', 'Protect CF7 forms'], + 'protect_elementor' => ['Elementor Forms', 'Protect Elementor Pro form widget'], + 'protect_gravity' => ['Gravity Forms', 'Protect Gravity Forms'], + 'protect_search' => ['Search Form', 'Protect the WordPress search form'], + ]; + foreach ($toggles as $key => [$label, $desc]): + $this->render_toggle('itk_honeypot', $key, $label, $desc, $opts); + endforeach; + ?> +
+ +
+

Timing Rules

+
+ + + + + + + + + + + + + + + +
Minimum Submit Time (seconds) + +

Block submissions faster than this (bots submit instantly).

+
Maximum Submit Time (seconds) + +

Block submissions older than this (stale/replayed forms).

+
Retain Logs (days) + +

Automatically prune logs older than this many days.

+
+ +
+
+
+ self::PER_PAGE, 'offset' => $offset]; + if ($search) $args['search'] = $search; + if ($filter_ip) $args['ip'] = $filter_ip; + if ($filter_bt) $args['bot_type'] = $filter_bt; + if ($filter_ac) $args['action'] = $filter_ac; + + $rows = ITK_Database::get_bot_rows($args); + $total = ITK_Database::count_bot_rows($args); + $bot_types = ITK_Database::get_bot_types(); + $total_pages= max(1, (int)ceil($total / self::PER_PAGE)); + + $base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=bot-logs'); + ?> +
+ +
+ + + + + + + + Reset +
+ +

Showing of result(s)

+ + + + + + + + + + + + + action === 'rate_limited' ? 'itk-badge-warn' : 'itk-badge-block'; + ?> + + + + + + + + + + + + +
Date / TimeIPBot TypeActionReasonURIUser Agent
No bot activity logged yet.
logged_at) ?> + ip_address) ?> + [filter] + [lookup] + bot_type) ?>action) ?>reason) ?>request_uri, 0, 80)) ?>user_agent, 0, 100)) ?>
+ + render_pager($paged, $total_pages, $base_url); ?> + + +
+ + + +
+
+ self::PER_PAGE, 'offset' => $offset]; + if ($search) $args['search'] = $search; + if ($filter_ip) $args['ip'] = $filter_ip; + if ($filter_form) $args['form'] = $filter_form; + + $rows = ITK_Database::get_honeypot_rows($args); + $total = ITK_Database::count_honeypot_rows($args); + $form_types = ITK_Database::get_honeypot_form_types(); + $total_pages = max(1, (int)ceil($total / self::PER_PAGE)); + + $base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=honeypot-logs'); + ?> +
+
+ + + + + + + Reset +
+ +

Showing of result(s)

+ + + + + + + + + + + + + + + + + + + + + + + + +
Date / TimeIPForm TypeReasonURIUser Agent
No honeypot catches yet.
blocked_at) ?> + ip_address) ?> + [filter] + [lookup] + form_type) ?>reason) ?>request_uri, 0, 80)) ?>user_agent, 0, 100)) ?>
+ + render_pager($paged, $total_pages, $base_url); ?> + +
+ + + +
+
+ ['Bad Bots', 'config/badbots.conf', 'One bot user-agent substring per line. Lines starting with # are comments.'], + 'goodbots' => ['Good Bots', 'config/goodbots.conf', 'Format: BotName|rate_per_minute (0 = always block)'], + 'referrers' => ['Bad Referrers', 'config/referrers.conf', 'One domain substring per line.'], + 'networks' => ['Bad Networks', 'config/networks.conf', 'One IP or CIDR range per line (e.g. 1.2.3.0/24).'], + 'allowed-ips' => ['Allowed IPs', 'config/allowed-ips.conf','IPs/CIDRs allowed to access wp-login.php (one per line).'], + ]; + + $active_file = sanitize_key($_GET['file'] ?? 'badbots'); + if (!isset($files[$active_file])) $active_file = 'badbots'; + [$title, $path, $desc] = $files[$active_file]; + $full_path = ITK_PATH . $path; + $content = file_exists($full_path) ? file_get_contents($full_path) : ''; + ?> +
+
+ [$label]): ?> + + +
+ +
+

+

+ +

+ + +

+
+
+ +
+
+ + +
+ +
+ '; + if ($paged > 1) { + echo '« Prev'; + } + echo '' . sprintf('Page %d of %d', $paged, $total_pages) . ''; + if ($paged < $total_pages) { + echo 'Next »'; + } + echo ''; + } +} diff --git a/includes/class-itk-bot-blocker.php b/includes/class-itk-bot-blocker.php new file mode 100644 index 0000000..765df61 --- /dev/null +++ b/includes/class-itk-bot-blocker.php @@ -0,0 +1,305 @@ +badbots_file = ITK_PATH . 'config/badbots.conf'; + $this->referrers_file = ITK_PATH . 'config/referrers.conf'; + $this->networks_file = ITK_PATH . 'config/networks.conf'; + $this->goodbots_file = ITK_PATH . 'config/goodbots.conf'; + + // Always hook; each method guards itself with its own option check. + add_action('init', [$this, 'check_request'], 1); + add_filter('robots_txt', [$this, 'modify_robots_txt'], 10, 2); + } + + /* ── Main entry point ─────────────────────────────────────── */ + + public function check_request(): void { + // Never block logged-in admins. + if (is_admin() || (function_exists('current_user_can') && current_user_can('manage_options'))) { + return; + } + + $options = get_option('itk_security', []); + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $referrer = $_SERVER['HTTP_REFERER'] ?? ''; + $ip = $this->get_client_ip(); + $uri = $_SERVER['REQUEST_URI'] ?? ''; + + // ── 1. Rate-limit good/legitimate bots ───────────────── + if (!empty($options['rate_limit_good_bots'])) { + $good_bot = $this->identify_good_bot($ua); + if ($good_bot !== null) { + $this->handle_good_bot($good_bot, $ua, $ip, $uri); + return; // Handled – don't fall through to block checks. + } + } + + // ── 2. Block OpenAI bots ─────────────────────────────── + if (!empty($options['block_openai_bots']) && $this->is_openai_bot($ua)) { + $this->block('OpenAI bot detected', 'openai', $ua, $referrer, $ip, $uri, $options); + } + + // ── 3. Block malicious bots ──────────────────────────── + if (!empty($options['block_malicious_bots']) && $this->is_malicious_bot($ua)) { + $this->block('Malicious bot detected', 'malicious_bot', $ua, $referrer, $ip, $uri, $options); + } + + // ── 4. Block bad referrers ───────────────────────────── + if (!empty($options['block_bad_referrers']) && $this->is_bad_referrer($referrer)) { + $this->block('Bad referrer detected', 'bad_referrer', $ua, $referrer, $ip, $uri, $options); + } + + // ── 5. Block bad networks ────────────────────────────── + if (!empty($options['block_bad_networks']) && $this->is_bad_network($ip)) { + $this->block('IP in blocked network', 'bad_network', $ua, $referrer, $ip, $uri, $options); + } + } + + /* ── Good-bot rate limiting ───────────────────────────────── */ + + /** + * Returns ['name' => string, 'limit' => int] or null if not a known good bot. + * A limit of 0 means "never allow" (treat as blocked). + */ + private function identify_good_bot(string $ua): ?array { + if (empty($ua)) return null; + + $cache_key = 'itk_goodbots_list'; + $list = get_transient($cache_key); + if ($list === false) { + $list = []; + if (file_exists($this->goodbots_file)) { + foreach (file($this->goodbots_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') continue; + $parts = explode('|', $line, 2); + $list[] = ['name' => trim($parts[0]), 'limit' => isset($parts[1]) ? (int)$parts[1] : 30]; + } + } + set_transient($cache_key, $list, 300); + } + + foreach ($list as $entry) { + if (stripos($ua, $entry['name']) !== false) { + return $entry; + } + } + return null; + } + + private function handle_good_bot(array $bot, string $ua, string $ip, string $uri): void { + $options = get_option('itk_security', []); + $name = $bot['name']; + $limit = (int)$bot['limit']; + + // Limit of 0 = always block this "good" bot (e.g. GPTBot still in goodbots.conf) + if ($limit === 0) { + $this->block("Good bot with limit 0: {$name}", $name, $ua, '', $ip, $uri, $options); + return; + } + + // Sliding window: track hits per bot per minute using transients. + $window = (int)(time() / 60); // 1-minute window + $tk_key = 'itk_rl_' . md5($name) . '_' . $window; + $count = (int)get_transient($tk_key); + + if ($count >= $limit) { + // Over the limit – log and send 429. + if (!empty($options['log_blocked_attempts'])) { + ITK_Database::log_bot([ + 'ip' => $ip, + 'ua' => $ua, + 'referrer' => '', + 'uri' => $uri, + 'bot_type' => $name, + 'reason' => "Rate limited: {$count}/{$limit} req/min", + 'action' => 'rate_limited', + ]); + } + status_header(429); + header('Retry-After: 60'); + header('X-ITK-Rate-Limit: ' . $limit); + echo 'Too Many Requests. Crawl-delay: 60'; + exit; + } + + // Under the limit – increment counter and allow through. + set_transient($tk_key, $count + 1, 120); + } + + /* ── Blocking ─────────────────────────────────────────────── */ + + private function block( + string $reason, + string $bot_type, + string $ua, + string $referrer, + string $ip, + string $uri, + array $options + ): void { + if (!empty($options['log_blocked_attempts'])) { + ITK_Database::log_bot([ + 'ip' => $ip, + 'ua' => $ua, + 'referrer' => $referrer, + 'uri' => $uri, + 'bot_type' => $bot_type, + 'reason' => $reason, + 'action' => 'blocked', + ]); + } + + $code = $options['response_code'] ?? '403'; + $message = $options['custom_message'] ?? 'Access denied.'; + $redir = $options['redirect_url'] ?? ''; + + if ($code === '301_custom' && !empty($redir)) { + header('Location: ' . esc_url_raw($redir), true, 301); + } else { + status_header((int)$code ?: 403); + echo esc_html($message); + } + exit; + } + + /* ── Detection helpers ────────────────────────────────────── */ + + private function is_openai_bot(string $ua): bool { + if (empty($ua)) return false; + foreach (['GPTBot', 'ChatGPT-User', 'OAI-SearchBot', 'whisper'] as $b) { + if (stripos($ua, $b) !== false) return true; + } + return false; + } + + private function is_malicious_bot(string $ua): bool { + if (empty($ua)) return false; + foreach ($this->load_conf_list($this->badbots_file, 'itk_bots_list') as $bot) { + if (stripos($ua, $bot) !== false) return true; + } + return false; + } + + private function is_bad_referrer(string $referrer): bool { + if (empty($referrer)) return false; + foreach ($this->load_conf_list($this->referrers_file, 'itk_referrers_list') as $ref) { + if (stripos($referrer, $ref) !== false) return true; + } + return false; + } + + private function is_bad_network(string $ip): bool { + if (empty($ip) || $ip === 'UNKNOWN') return false; + foreach ($this->load_conf_list($this->networks_file, 'itk_networks_list') as $network) { + if (filter_var($network, FILTER_VALIDATE_IP)) { + if ($ip === $network) return true; + } elseif (strpos($network, '/') !== false) { + if ($this->ip_in_cidr($ip, $network)) return true; + } + } + return false; + } + + /* ── Robots.txt ───────────────────────────────────────────── */ + + public function modify_robots_txt(string $output, string $public): string { + if ($public === '0') return $output; + $options = get_option('itk_security', []); + if (empty($options['block_openai_bots'])) return $output; + + $output .= "\n# InformatiQ Toolkit – AI bot disallow\n"; + foreach (['GPTBot', 'ChatGPT-User', 'OAI-SearchBot'] as $bot) { + $output .= "User-agent: {$bot}\nDisallow: /\n\n"; + } + return $output; + } + + /* ── Config file readers ──────────────────────────────────── */ + + private function load_conf_list(string $file, string $cache_key): array { + $cached = get_transient($cache_key); + if ($cached !== false) return $cached; + + if (!file_exists($file) || filesize($file) > 1048576) return []; + + $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $list = []; + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') continue; + if (strlen($line) <= 200 && !preg_match('/[<>"\']/', $line)) { + $list[] = $line; + } + } + + set_transient($cache_key, $list, 300); + return $list; + } + + public function invalidate_cache(): void { + delete_transient('itk_bots_list'); + delete_transient('itk_referrers_list'); + delete_transient('itk_networks_list'); + delete_transient('itk_goodbots_list'); + } + + /* ── IP utilities ─────────────────────────────────────────── */ + + public function get_client_ip(): string { + $keys = [ + 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', + 'REMOTE_ADDR', + ]; + foreach ($keys as $key) { + if (empty($_SERVER[$key])) continue; + $ip = trim(explode(',', $_SERVER[$key])[0]); + if ($key !== 'REMOTE_ADDR' && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return $ip; + } + if ($key === 'REMOTE_ADDR' && filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + return 'UNKNOWN'; + } + + private function ip_in_cidr(string $ip, string $cidr): bool { + if (strpos($cidr, '/') === false) return false; + [$subnet, $mask] = explode('/', $cidr, 2); + if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; + if (!is_numeric($mask) || $mask < 0 || $mask > 32) return false; + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; + + $ip_long = ip2long($ip); + $sub_long = ip2long($subnet); + $mask_dec = ~((1 << (32 - (int)$mask)) - 1); + return ($ip_long & $mask_dec) === ($sub_long & $mask_dec); + } + + /* ── Accessors for admin ──────────────────────────────────── */ + + public function get_badbots_file(): string { return $this->badbots_file; } + public function get_referrers_file(): string { return $this->referrers_file; } + public function get_networks_file(): string { return $this->networks_file; } + public function get_goodbots_file(): string { return $this->goodbots_file; } +} diff --git a/includes/class-itk-database.php b/includes/class-itk-database.php new file mode 100644 index 0000000..b773b8b --- /dev/null +++ b/includes/class-itk-database.php @@ -0,0 +1,283 @@ +prefix . 'itk_bot_log'; + } + + public static function honeypot_table(): string { + global $wpdb; + return $wpdb->prefix . 'itk_honeypot_log'; + } + + /* ── Install / upgrade ────────────────────────────────────── */ + + public static function install() { + global $wpdb; + $charset = $wpdb->get_charset_collate(); + + $sql_bot = "CREATE TABLE " . self::bot_table() . " ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + logged_at DATETIME NOT NULL, + ip_address VARCHAR(45) NOT NULL DEFAULT '', + user_agent TEXT NOT NULL, + referrer VARCHAR(1000) NOT NULL DEFAULT '', + request_uri VARCHAR(1000) NOT NULL DEFAULT '', + bot_type VARCHAR(100) NOT NULL DEFAULT '', + reason VARCHAR(255) NOT NULL DEFAULT '', + action VARCHAR(20) NOT NULL DEFAULT 'blocked', + PRIMARY KEY (id), + KEY ip_address (ip_address), + KEY logged_at (logged_at), + KEY bot_type (bot_type), + KEY action (action) + ) {$charset};"; + + $sql_hp = "CREATE TABLE " . self::honeypot_table() . " ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + blocked_at DATETIME NOT NULL, + ip_address VARCHAR(45) NOT NULL DEFAULT '', + form_type VARCHAR(100) NOT NULL DEFAULT '', + reason VARCHAR(255) NOT NULL DEFAULT '', + request_uri VARCHAR(1000) NOT NULL DEFAULT '', + user_agent TEXT NOT NULL, + PRIMARY KEY (id), + KEY ip_address (ip_address), + KEY blocked_at (blocked_at), + KEY form_type (form_type) + ) {$charset};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql_bot); + dbDelta($sql_hp); + + update_option(self::DB_VERSION_OPTION, self::DB_VERSION); + } + + /* ── Bot log ──────────────────────────────────────────────── */ + + public static function log_bot(array $data): void { + global $wpdb; + $wpdb->insert( + self::bot_table(), + [ + 'logged_at' => current_time('mysql'), + 'ip_address' => sanitize_text_field($data['ip'] ?? ''), + 'user_agent' => sanitize_textarea_field($data['ua'] ?? ''), + 'referrer' => esc_url_raw(substr($data['referrer'] ?? '', 0, 1000)), + 'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)), + 'bot_type' => sanitize_text_field($data['bot_type'] ?? ''), + 'reason' => sanitize_text_field($data['reason'] ?? ''), + 'action' => sanitize_text_field($data['action'] ?? 'blocked'), + ], + ['%s','%s','%s','%s','%s','%s','%s','%s'] + ); + } + + public static function get_bot_rows(array $args = []): array { + global $wpdb; + $table = self::bot_table(); + $limit = max(1, (int)($args['per_page'] ?? 25)); + $offset = max(0, (int)($args['offset'] ?? 0)); + $where = '1=1'; + $params = []; + + if (!empty($args['action'])) { + $where .= ' AND action = %s'; + $params[] = $args['action']; + } + if (!empty($args['bot_type'])) { + $where .= ' AND bot_type = %s'; + $params[] = $args['bot_type']; + } + if (!empty($args['ip'])) { + $where .= ' AND ip_address = %s'; + $params[] = $args['ip']; + } + if (!empty($args['search'])) { + $like = '%' . $wpdb->esc_like($args['search']) . '%'; + $where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)'; + $params[] = $like; $params[] = $like; $params[] = $like; + } + + $params[] = $limit; + $params[] = $offset; + $sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY logged_at DESC LIMIT %d OFFSET %d"; + return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: []; + } + + public static function count_bot_rows(array $args = []): int { + global $wpdb; + $table = self::bot_table(); + $where = '1=1'; + $params = []; + + if (!empty($args['action'])) { + $where .= ' AND action = %s'; + $params[] = $args['action']; + } + if (!empty($args['bot_type'])) { + $where .= ' AND bot_type = %s'; + $params[] = $args['bot_type']; + } + if (!empty($args['ip'])) { + $where .= ' AND ip_address = %s'; + $params[] = $args['ip']; + } + if (!empty($args['search'])) { + $like = '%' . $wpdb->esc_like($args['search']) . '%'; + $where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)'; + $params[] = $like; $params[] = $like; $params[] = $like; + } + + $sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}"; + return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql)); + } + + public static function get_bot_stats(): array { + global $wpdb; + $table = self::bot_table(); + $today = current_time('Y-m-d'); + + return [ + 'total' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}"), + 'today' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(logged_at)=%s", $today)), + 'blocked' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE action='blocked'"), + 'rate_limited' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE action='rate_limited'"), + 'top_bot_types' => $wpdb->get_results("SELECT bot_type, COUNT(*) as cnt FROM {$table} WHERE bot_type != '' GROUP BY bot_type ORDER BY cnt DESC LIMIT 8") ?: [], + 'top_ips' => $wpdb->get_results("SELECT ip_address, COUNT(*) as cnt FROM {$table} GROUP BY ip_address ORDER BY cnt DESC LIMIT 5") ?: [], + 'last_24h_counts' => $wpdb->get_results("SELECT DATE_FORMAT(logged_at,'%H:00') as hour, COUNT(*) as cnt FROM {$table} WHERE logged_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY hour ORDER BY hour ASC") ?: [], + ]; + } + + public static function get_bot_types(): array { + global $wpdb; + return $wpdb->get_col("SELECT DISTINCT bot_type FROM " . self::bot_table() . " WHERE bot_type != '' ORDER BY bot_type ASC") ?: []; + } + + public static function clear_bot_log(): void { + global $wpdb; + $wpdb->query("TRUNCATE TABLE " . self::bot_table()); + } + + public static function prune_bot_log(int $days): void { + global $wpdb; + $wpdb->query($wpdb->prepare( + "DELETE FROM " . self::bot_table() . " WHERE logged_at < DATE_SUB(NOW(), INTERVAL %d DAY)", + $days + )); + } + + /* ── Honeypot log ─────────────────────────────────────────── */ + + public static function log_honeypot(array $data): void { + global $wpdb; + $wpdb->insert( + self::honeypot_table(), + [ + 'blocked_at' => current_time('mysql'), + 'ip_address' => sanitize_text_field($data['ip'] ?? ''), + 'form_type' => sanitize_text_field($data['form'] ?? 'Unknown'), + 'reason' => sanitize_text_field($data['reason'] ?? ''), + 'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)), + 'user_agent' => sanitize_textarea_field($data['ua'] ?? ''), + ], + ['%s','%s','%s','%s','%s','%s'] + ); + } + + public static function get_honeypot_rows(array $args = []): array { + global $wpdb; + $table = self::honeypot_table(); + $limit = max(1, (int)($args['per_page'] ?? 25)); + $offset = max(0, (int)($args['offset'] ?? 0)); + $where = '1=1'; + $params = []; + + if (!empty($args['form'])) { + $where .= ' AND form_type = %s'; + $params[] = $args['form']; + } + if (!empty($args['ip'])) { + $where .= ' AND ip_address = %s'; + $params[] = $args['ip']; + } + if (!empty($args['search'])) { + $like = '%' . $wpdb->esc_like($args['search']) . '%'; + $where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)'; + $params[] = $like; $params[] = $like; $params[] = $like; + } + + $params[] = $limit; + $params[] = $offset; + $sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY blocked_at DESC LIMIT %d OFFSET %d"; + return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: []; + } + + public static function count_honeypot_rows(array $args = []): int { + global $wpdb; + $table = self::honeypot_table(); + $where = '1=1'; + $params = []; + + if (!empty($args['form'])) { + $where .= ' AND form_type = %s'; + $params[] = $args['form']; + } + if (!empty($args['ip'])) { + $where .= ' AND ip_address = %s'; + $params[] = $args['ip']; + } + if (!empty($args['search'])) { + $like = '%' . $wpdb->esc_like($args['search']) . '%'; + $where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)'; + $params[] = $like; $params[] = $like; $params[] = $like; + } + + $sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}"; + return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql)); + } + + public static function get_honeypot_stats(): array { + global $wpdb; + $table = self::honeypot_table(); + $today = current_time('Y-m-d'); + + return [ + 'total' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}"), + 'today' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(blocked_at)=%s", $today)), + 'top_forms' => $wpdb->get_results("SELECT form_type, COUNT(*) as cnt FROM {$table} GROUP BY form_type ORDER BY cnt DESC LIMIT 8") ?: [], + 'top_ips' => $wpdb->get_results("SELECT ip_address, COUNT(*) as cnt FROM {$table} GROUP BY ip_address ORDER BY cnt DESC LIMIT 5") ?: [], + ]; + } + + public static function get_honeypot_form_types(): array { + global $wpdb; + return $wpdb->get_col("SELECT DISTINCT form_type FROM " . self::honeypot_table() . " ORDER BY form_type ASC") ?: []; + } + + public static function clear_honeypot_log(): void { + global $wpdb; + $wpdb->query("TRUNCATE TABLE " . self::honeypot_table()); + } + + public static function prune_honeypot_log(int $days): void { + global $wpdb; + $wpdb->query($wpdb->prepare( + "DELETE FROM " . self::honeypot_table() . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)", + $days + )); + } +} diff --git a/includes/class-itk-honeypot.php b/includes/class-itk-honeypot.php new file mode 100644 index 0000000..45fd11a --- /dev/null +++ b/includes/class-itk-honeypot.php @@ -0,0 +1,277 @@ +.itk-hp-field{display:none!important;visibility:hidden!important;opacity:0!important;position:absolute!important;left:-9999px!important;top:-9999px!important;}' . "\n"; + } + + /* ── JS token (HMAC-based anti-CSRF) ─────────────────────── */ + + public function enqueue_token_script(): void { + $secret = $this->get_page_secret(); + wp_add_inline_script('jquery-core', " +(function(){ + var s='" . esc_js($secret) . "',n='" . esc_js(wp_create_nonce('itk_hp')) . "'; + document.querySelectorAll('." . self::TOKEN_FIELD . "').forEach(function(f){ + f.value=btoa(s+'|'+Date.now()+'|'+n); + }); + document.querySelectorAll('." . self::TIME_FIELD . "').forEach(function(f){ + f.value=Math.floor(Date.now()/1000); + }); +})(); +", 'after'); + } + + /* ── Field HTML generator ─────────────────────────────────── */ + + private function honeypot_html(string $form_type = ''): string { + $field = self::FIELD_PREFIX . substr(md5(uniqid()), 0, 8); + $ts = time(); + $label = ['Your email address', 'Website URL', 'Full name'][array_rand(['a','b','c'])]; + return sprintf( + '', + esc_html($label), + esc_attr($field), + self::TOKEN_FIELD, self::TOKEN_FIELD, + self::TIME_FIELD, self::TIME_FIELD, + $ts + ); + } + + /* ── Injectors ────────────────────────────────────────────── */ + + public function inject_generic(): void { + if (empty(self::$opts['enabled'])) return; + echo $this->honeypot_html(); // phpcs:ignore + } + + public function inject_comment(): void { + if (empty(self::$opts['protect_comments'])) return; + echo $this->honeypot_html('comment'); // phpcs:ignore + } + + public function inject_search(string $form): string { + return $form . $this->honeypot_html('search'); + } + + public function inject_cf7(string $content): string { + return $content . $this->honeypot_html('cf7'); + } + + public function inject_gravity(string $tag, array $form): string { + return $tag . $this->honeypot_html('gravity'); + } + + public function elementor_enqueue(): void { + // Elementor injects via JS – add hidden fields via wp_footer + add_action('wp_footer', [$this, 'inject_generic']); + } + + /* ── Validators ───────────────────────────────────────────── */ + + private function check_honeypot(string $form_type): bool { + // 1. Honeypot field must be empty + foreach ($_POST as $key => $val) { + if (strpos($key, self::FIELD_PREFIX) === 0 && !empty($val)) { + $this->log_block($form_type, 'Honeypot field filled'); + return false; + } + } + + // 2. Timing check + $opts = get_option('itk_honeypot', []); + $min_t = max(1, (int)($opts['min_time'] ?? 3)); + $max_t = max(60, (int)($opts['max_time'] ?? 7200)); + $ts = (int)($_POST[self::TIME_FIELD] ?? 0); + $elapsed = time() - $ts; + + if ($ts > 0 && $elapsed < $min_t) { + $this->log_block($form_type, "Submitted too fast ({$elapsed}s)"); + return false; + } + if ($ts > 0 && $elapsed > $max_t) { + $this->log_block($form_type, "Submitted too slow ({$elapsed}s > {$max_t}s)"); + return false; + } + + return true; + } + + private function log_block(string $form_type, string $reason): void { + ITK_Database::log_honeypot([ + 'ip' => $this->get_ip(), + 'form' => $form_type, + 'reason' => $reason, + 'uri' => $_SERVER['REQUEST_URI'] ?? '', + 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', + ]); + } + + public function validate_comment(array $comment_data): array { + if (empty(self::$opts['protect_comments'])) return $comment_data; + if (!$this->check_honeypot('comment')) { + wp_die('Spam detected. Please go back and try again.', 'Spam Blocked', ['response' => 403]); + } + return $comment_data; + } + + public function validate_login($user, string $username, string $password) { + if (empty(self::$opts['protect_login'])) return $user; + if (!empty($username) && !$this->check_honeypot('login')) { + return new WP_Error('honeypot_blocked', 'Access denied.'); + } + return $user; + } + + public function validate_register($sanitized_user_login, $user_email, \WP_Error $errors): void { + if (empty(self::$opts['protect_register'])) return; + if (!$this->check_honeypot('register')) { + $errors->add('honeypot_blocked', 'Spam registration detected.'); + } + } + + public function validate_lost_password(\WP_Error $errors): void { + if (empty(self::$opts['protect_lost_password'])) return; + if (!$this->check_honeypot('lostpassword')) { + $errors->add('honeypot_blocked', 'Access denied.'); + } + } + + public function validate_woo_checkout(): void { + if (!$this->check_honeypot('woo_checkout')) { + wc_add_notice('Spam submission detected. Please refresh and try again.', 'error'); + } + } + + public function validate_woo_registration(\WP_Error $errors, $username, $email): \WP_Error { + if (!$this->check_honeypot('woo_register')) { + $errors->add('honeypot_blocked', 'Spam registration detected.'); + } + return $errors; + } + + public function validate_cf7($contact_form, &$abort, $submission): void { + if (!$this->check_honeypot('cf7')) { + $abort = true; + $contact_form->set_status('spam'); + } + } + + public function validate_elementor($record, $ajax_handler): void { + if (!$this->check_honeypot('elementor')) { + $ajax_handler->add_error_message('Spam detected. Please try again.'); + } + } + + public function validate_gravity(array $validation_result): array { + if (!$this->check_honeypot('gravity')) { + $validation_result['is_valid'] = false; + foreach ($validation_result['form']['fields'] as &$field) { + $field->failed_validation = true; + $field->validation_message = 'Spam detected.'; + } + } + return $validation_result; + } + + /* ── Helpers ──────────────────────────────────────────────── */ + + private function get_ip(): string { + $keys = [ + 'HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR', + ]; + foreach ($keys as $k) { + if (!empty($_SERVER[$k])) { + $ip = trim(explode(',', $_SERVER[$k])[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; + } + } + return 'UNKNOWN'; + } + + private function get_page_secret(): string { + $secret = get_option('itk_hp_secret'); + if (!$secret) { + $secret = wp_generate_password(32, false); + update_option('itk_hp_secret', $secret); + } + return $secret; + } +} diff --git a/includes/class-itk-optimization.php b/includes/class-itk-optimization.php new file mode 100644 index 0000000..f89ff7f --- /dev/null +++ b/includes/class-itk-optimization.php @@ -0,0 +1,359 @@ +opts = get_option('itk_optimization', []); + $this->init(); + } + + private function on(string $key): bool { + return !empty($this->opts[$key]); + } + + private function init(): void { + // ── Version / meta ────────────────────────────────────── + if ($this->on('remove_wp_version')) { + remove_action('wp_head', 'wp_generator'); + add_filter('the_generator', '__return_empty_string'); + } + + // ── Login errors ──────────────────────────────────────── + if ($this->on('hide_login_errors')) { + add_filter('login_errors', fn() => 'Something went wrong.'); + } + + // ── Comment class ─────────────────────────────────────── + if ($this->on('remove_author_class')) { + add_filter('comment_class', [$this, 'remove_comment_author_class']); + } + + // ── Script / style versions ───────────────────────────── + if ($this->on('remove_script_versions')) { + add_filter('style_loader_src', [$this, 'remove_version_param'], 999); + add_filter('script_loader_src', [$this, 'remove_version_param'], 999); + } + + // ── Author base ───────────────────────────────────────── + if ($this->on('change_author_base')) { + add_action('init', [$this, 'change_author_base']); + } + + // ── Revisions ─────────────────────────────────────────── + if ($this->on('limit_revisions') && !defined('WP_POST_REVISIONS')) { + define('WP_POST_REVISIONS', 3); + } + if (!defined('AUTOSAVE_INTERVAL')) { + define('AUTOSAVE_INTERVAL', 300); + } + + // ── Emoji ─────────────────────────────────────────────── + if ($this->on('remove_emoji')) { + add_action('init', [$this, 'disable_emojis']); + } + + // ── User fields ───────────────────────────────────────── + if ($this->on('remove_default_userfields')) { + add_filter('user_contactmethods', [$this, 'remove_default_userfields']); + } + + // ── Content cleanup ───────────────────────────────────── + if ($this->on('clean_bad_content')) { + add_filter('content_save_pre', [$this, 'clean_bad_content']); + } + + // ── WP head noise ─────────────────────────────────────── + if ($this->on('remove_wp_head_noise')) { + $this->remove_wp_head_noise(); + } + + // ── XML-RPC ───────────────────────────────────────────── + if ($this->on('disable_xml_rpc')) { + add_filter('xmlrpc_enabled', '__return_false'); + } + + // ── WP Embed ──────────────────────────────────────────── + if ($this->on('deregister_wp_embed')) { + add_action('wp_footer', [$this, 'deregister_wp_embed']); + } + + // ── Empty search ──────────────────────────────────────── + if ($this->on('stop_empty_search_redirect')) { + add_filter('request', [$this, 'stop_empty_search']); + } + + // ── Widgets ───────────────────────────────────────────── + if ($this->on('unregister_default_widgets')) { + add_action('widgets_init', [$this, 'unregister_default_widgets'], 11); + } + + // ── Defer JS ──────────────────────────────────────────── + if ($this->on('defer_js')) { + add_filter('script_loader_tag', [$this, 'defer_js'], 10); + } + + // ── Heartbeat ─────────────────────────────────────────── + if ($this->on('limit_heartbeat')) { + add_filter('wpe_heartbeat_allowed_pages', [$this, 'limit_heartbeat_pages']); + } + + // ── Dashboard widgets ─────────────────────────────────── + if ($this->on('disable_dashboard_widgets')) { + add_action('admin_init', [$this, 'disable_dashboard_widgets'], 9999); + } + add_action('wp_dashboard_setup', [$this, 'add_itq_dashboard_widget']); + + // ── Comment URL field ─────────────────────────────────── + if ($this->on('disable_comments_url')) { + add_filter('comment_form_default_fields', [$this, 'remove_comment_url']); + } + + // ── Google FLoC / Permissions-Policy ──────────────────── + if ($this->on('disable_floc')) { + add_filter('wp_headers', [$this, 'disable_floc']); + } + + // ── Lightbox images ───────────────────────────────────── + if ($this->on('lightbox_images')) { + add_filter('the_content', [$this, 'add_lightbox_rel']); + } + + // ── Admin bar cleanup ─────────────────────────────────── + if ($this->on('remove_admin_bar_links')) { + add_action('wp_before_admin_bar_render', [$this, 'remove_admin_bar_links']); + } + + // ── Admin branding ────────────────────────────────────── + if ($this->on('admin_branding')) { + add_filter('admin_footer_text', [$this, 'admin_footer_text']); + add_action('admin_bar_menu', [$this, 'toolbar_link'], 999); + add_action('admin_bar_menu', [$this, 'remove_wp_logo'], 999); + add_action('admin_notices', [$this, 'admin_notice']); + } + + // ── RSS featured image ────────────────────────────────── + if ($this->on('featured_image_rss')) { + add_filter('the_excerpt_rss', [$this, 'featured_to_rss']); + add_filter('the_content_feed', [$this, 'featured_to_rss']); + } + + // ── DNS prefetch ──────────────────────────────────────── + if ($this->on('dns_prefetch')) { + add_action('wp_head', [$this, 'dns_prefetch'], 1); + } + + // ── Google jQuery ─────────────────────────────────────── + if ($this->on('use_google_jquery')) { + add_action('init', [$this, 'use_google_jquery']); + } + } + + /* ── Callback implementations ─────────────────────────────── */ + + public function remove_comment_author_class(array $classes): array { + return array_filter($classes, fn($c) => strpos($c, 'comment-author-') === false); + } + + public function remove_version_param(string $src): string { + return strpos($src, 'ver=') ? remove_query_arg('ver', $src) : $src; + } + + public function change_author_base(): void { + global $wp_rewrite; + $wp_rewrite->author_base = 'writer'; + } + + public function disable_emojis(): void { + remove_action('wp_head', 'print_emoji_detection_script', 7); + remove_action('admin_print_scripts','print_emoji_detection_script'); + remove_action('wp_print_styles', 'print_emoji_styles'); + remove_action('admin_print_styles','print_emoji_styles'); + remove_filter('the_content_feed', 'wp_staticize_emoji'); + remove_filter('comment_text_rss', 'wp_staticize_emoji'); + remove_filter('wp_mail', 'wp_staticize_emoji_for_email'); + add_filter('tiny_mce_plugins', fn($p) => is_array($p) ? array_diff($p, ['wpemoji']) : []); + } + + public function remove_default_userfields(array $fields): array { + foreach (['aim','jabber','yim'] as $f) unset($fields[$f]); + return $fields; + } + + public function clean_bad_content(string $content): string { + return preg_replace([ + "~]*>\s?

~", + "~]*>\s?~", + "~]*>~", + "~<\/font>~", + "~style\=\"[^\"]*\"~", + "~]*>\s?~", + ], '', $content) ?? $content; + } + + private function remove_wp_head_noise(): void { + remove_action('wp_head', 'wlwmanifest_link'); + remove_action('wp_head', 'rsd_link'); + remove_action('wp_head', 'wp_generator'); + remove_action('wp_head', 'start_post_rel_link'); + remove_action('wp_head', 'index_rel_link'); + remove_action('wp_head', 'feed_links_extra', 3); + remove_action('wp_head', 'feed_links', 2); + remove_action('wp_head', 'parent_post_rel_link', 10, 0); + remove_action('wp_head', 'start_post_rel_link', 10, 0); + remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0); + } + + public function deregister_wp_embed(): void { + wp_deregister_script('wp-embed'); + } + + public function stop_empty_search(array $vars): array { + if (isset($_GET['s']) && empty($_GET['s'])) $vars['s'] = ' '; + return $vars; + } + + public function unregister_default_widgets(): void { + foreach ([ + 'WP_Widget_Calendar', 'WP_Widget_Archives', 'WP_Widget_Meta', + 'WP_Widget_Search', 'WP_Widget_Tag_Cloud', + ] as $w) { + if (class_exists($w)) unregister_widget($w); + } + } + + public function defer_js(string $tag): string { + $defer = [ + 'owl-carousel.min.js','mansonry.js','imgloaded.js', + 'jquery.magnific-popup.min.js','bgswitcher.js','exit.js', + 'lazyload.js','app.js', + 'add-to-cart.min.js','cart-fragments.min.js','woocommerce.min.js', + 'wp-embed.min.js', + ]; + foreach ($defer as $s) { + if (strpos($tag, $s) !== false) { + return str_replace(' src', ' defer="defer" src', $tag); + } + } + return $tag; + } + + public function limit_heartbeat_pages(array $allowed): array { + return ['index.php','admin.php','edit.php','post.php','post-new.php']; + } + + public function disable_dashboard_widgets(): void { + remove_meta_box('dashboard_primary', 'dashboard', 'core'); + remove_meta_box('wpe_dify_news_feed', 'dashboard', 'normal'); + global $wp_meta_boxes; + unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_right_now']); + unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_secondary']); + unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_quick_press']); + } + + public function add_itq_dashboard_widget(): void { + wp_add_dashboard_widget( + 'itk_info_widget', + 'Developed & Maintained by', + [$this, 'itq_dashboard_widget_content'] + ); + } + + public function itq_dashboard_widget_content(): void { + echo '
' + . '' + . '' + . '
Strategic Solutions, Intelligent IT Services' + . '

Email: support@informatiq.services' + . '
Phone: (+34) 971 560 060  |  Emergency: (+34) 643 732 407' + . '
'; + } + + public function remove_comment_url(array $fields): array { + unset($fields['url']); + return $fields; + } + + public function disable_floc(array $headers): array { + $headers['Permissions-Policy'] = 'interest-cohort=()'; + return $headers; + } + + public function add_lightbox_rel(string $content): string { + global $post; + $title = isset($post->post_title) ? esc_attr($post->post_title) : ''; + $pattern = '//i'; + $replace = ''; + return preg_replace($pattern, $replace, $content) ?? $content; + } + + public function remove_admin_bar_links(): void { + global $wp_admin_bar; + foreach (['wp-logo','about','wporg','documentation','support-forums','feedback','comments'] as $node) { + $wp_admin_bar->remove_menu($node); + } + } + + public function admin_footer_text(): string { + return 'WordPress Core | ' + . 'Customizations by InformatiQ Services'; + } + + public function toolbar_link(\WP_Admin_Bar $bar): void { + $bar->add_node([ + 'id' => 'itq-support', + 'title' => 'InformatiQ Services', + 'href' => 'https://informatiq.services', + 'meta' => ['class' => 'itq-support', 'title' => 'Strategic Solutions, Intelligent IT Services'], + ]); + } + + public function remove_wp_logo(\WP_Admin_Bar $bar): void { + $bar->remove_node('wp-logo'); + } + + public function admin_notice(): void { + echo '

' + . esc_html__('This website has been developed and is being hosted and maintained by', 'informatiq-toolkit') + . ' InformatiQ

'; + } + + public function featured_to_rss(string $content): string { + global $post; + if (!empty($post->ID) && has_post_thumbnail($post->ID)) { + $content = get_the_post_thumbnail($post->ID, 'thumbnail', ['style' => 'float:left;margin:0 15px 15px 0']) . $content; + } + return $content; + } + + public function dns_prefetch(): void { + echo "\n\n" + . '' . "\n" + . "\n"; + } + + public function use_google_jquery(): void { + if (is_admin()) return; + wp_deregister_script('jquery'); + wp_register_script('jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js', [], null, true); + wp_enqueue_script('jquery'); + } + + /* ── Option update (called by admin AJAX) ─────────────────── */ + + public function update_options(array $opts): void { + $this->opts = $opts; + } + + public function get_options(): array { + return $this->opts; + } +} diff --git a/includes/class-itk-protection.php b/includes/class-itk-protection.php new file mode 100644 index 0000000..c46be67 --- /dev/null +++ b/includes/class-itk-protection.php @@ -0,0 +1,306 @@ +allowed_ips_file = ITK_PATH . 'config/allowed-ips.conf'; + $this->load_allowed_ips(); + + add_action('init', [$this, 'protect_wp_login'], 0); + add_action('init', [$this, 'block_sensitive_files'], 0); + add_action('init', [$this, 'block_malicious_queries'], 0); + add_action('init', [$this, 'block_author_scans'], 0); + add_action('init', [$this, 'custom_login_url'], 0); + add_action('send_headers', [$this, 'add_security_headers']); + add_action('wp_loaded', [$this, 'wp_loaded_custom_login']); + + add_filter('the_generator', '__return_empty_string'); + add_filter('wp_redirect', [$this, 'redirect_filter'], 10, 2); + add_filter('network_site_url', [$this, 'network_url_filter'], 10, 3); + add_filter('site_url', [$this, 'site_url_filter'], 10, 4); + add_filter('wp_handle_upload_prefilter', [$this, 'filter_uploaded_files']); + } + + /* ── wp-login protection ──────────────────────────────────── */ + + public function protect_wp_login(): void { + $options = get_option('itk_security', []); + if (empty($options['protect_wp_login'])) return; + + $uri = $_SERVER['REQUEST_URI'] ?? ''; + if (strpos($uri, 'wp-login.php') === false) return; + + $ip = $this->get_client_ip(); + $is_allowed = false; + foreach ($this->allowed_ips as $allowed) { + if ($ip === $allowed) { $is_allowed = true; break; } + if (strpos($allowed, '/') !== false && $this->ip_in_cidr($ip, $allowed)) { + $is_allowed = true; break; + } + } + + if (!$is_allowed) { + $this->send_403($options, 'Access to login page not allowed from your IP address.'); + } + + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $protocol = $_SERVER['SERVER_PROTOCOL'] ?? ''; + if (empty($ua) || $protocol === 'HTTP/1.0') { + $this->send_403($options, 'Invalid request detected.'); + } + } + + /* ── Security headers ─────────────────────────────────────── */ + + public function add_security_headers(): void { + $options = get_option('itk_security', []); + if (empty($options['add_security_headers'])) return; + if (headers_sent()) return; + + header_remove('X-Powered-By'); + header('X-Content-Type-Options: nosniff'); + header('X-Frame-Options: SAMEORIGIN'); + header('X-XSS-Protection: 1; mode=block'); + header('Referrer-Policy: strict-origin-when-cross-origin'); + } + + /* ── Sensitive file blocking ──────────────────────────────── */ + + public function block_sensitive_files(): void { + $options = get_option('itk_security', []); + $uri = $_SERVER['REQUEST_URI'] ?? ''; + + if (!empty($options['protect_wp_includes'])) { + if (preg_match('#^/wp-includes/[^/]+\.php$#i', $uri)) { + $this->send_403($options, 'Access to this file is not allowed.'); + } + if (preg_match('#^/wp-admin/includes/#i', $uri)) { + $this->send_403($options, 'Access to this file is not allowed.'); + } + if (preg_match('#^/wp-includes/theme-compat/#i', $uri)) { + $this->send_403($options, 'Access to this file is not allowed.'); + } + if (preg_match('#/wp-includes/js/tinymce/langs/.+\.php#i', $uri)) { + $this->send_403($options, 'Access to this file is not allowed.'); + } + if (preg_match('#(license\.txt|wp-config-sample\.php|readme\.html)$#i', $uri)) { + $this->send_403($options, 'Access to this file is not allowed.'); + } + if (preg_match('#(?:^|/)\.(?!well-known)#', $uri)) { + $this->send_403($options, 'Access to hidden files is not allowed.'); + } + } + + if (!empty($options['protect_uploads'])) { + if (preg_match('#^/wp-content/uploads/.*\.(?:php[1-6]?|pht|phtml?)$#i', $uri)) { + $this->send_403($options, 'PHP files are not allowed in the uploads directory.'); + } + } + + if (!empty($options['block_xmlrpc'])) { + if (strpos($uri, 'xmlrpc.php') !== false) { + $this->send_403($options, 'XML-RPC is disabled on this site.'); + } + } + } + + /* ── Malicious query blocking ─────────────────────────────── */ + + public function block_malicious_queries(): void { + $options = get_option('itk_security', []); + if (empty($options['block_malicious_queries'])) return; + + $qs = $_SERVER['QUERY_STRING'] ?? ''; + if (empty($qs)) return; + + $patterns = [ + '(eval\()', + '(127\.0\.0\.1)', + '([a-z0-9]{2000})', + '(javascript:)(.*)(;)', + '(base64_encode)(.*)(\()', + '(GLOBALS|REQUEST)(=|\[|%)', + '(<|%3C)(.*)script(.*)(>|%3)', + '(boot\.ini|etc/passwd|self/environ)', + '(thumbs?(_editor|open)?|tim(thumb)?)\.php', + '(\'|\\")(.*)(drop|insert|md5|select|union)', + ]; + + foreach ($patterns as $pattern) { + if (preg_match('#' . $pattern . '#i', $qs)) { + $this->send_403($options, 'Malicious query detected.'); + } + } + + $method = strtolower($_SERVER['REQUEST_METHOD'] ?? ''); + if (preg_match('#^(connect|debug|delete|move|put|trace|track)$#', $method)) { + $this->send_403($options, 'This request method is not allowed.'); + } + } + + /* ── Author scan blocking ─────────────────────────────────── */ + + public function block_author_scans(): void { + $options = get_option('itk_security', []); + if (empty($options['block_author_scans'])) return; + + $uri = $_SERVER['REQUEST_URI'] ?? ''; + $qs = $_SERVER['QUERY_STRING'] ?? ''; + + if (strpos($uri, '/wp-admin') === false && preg_match('/author=\d+/i', $qs)) { + wp_redirect(home_url(), 301); + exit; + } + } + + /* ── Custom login URL ─────────────────────────────────────── */ + + public function custom_login_url(): void { + $options = get_option('itk_security', []); + if (empty($options['enable_custom_login'])) return; + + $slug = $this->custom_slug($options); + $path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''; + $qs = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? ''; + + if (strpos($path, '/' . $slug) !== false) { + if (!session_id()) session_start(); + $_SESSION['itk_login_access'] = time(); + require_once ABSPATH . 'wp-login.php'; + exit; + } + + $blocked = ['/wp-login.php', '/wp-admin/', '/login/', '/admin/']; + foreach ($blocked as $b) { + if (strpos($path, $b) !== false) { + if (defined('DOING_AJAX') && DOING_AJAX) return; + if (!session_id()) session_start(); + if (isset($_SESSION['itk_login_access']) && (time() - $_SESSION['itk_login_access']) < 300) return; + if (is_user_logged_in()) return; + $this->send_403($options, 'Access denied. Please use the correct login URL.'); + } + } + } + + public function wp_loaded_custom_login(): void { + $options = get_option('itk_security', []); + if (empty($options['enable_custom_login'])) return; + + global $pagenow; + if ($pagenow === 'wp-login.php') { + if (!session_id()) session_start(); + if (!isset($_SESSION['itk_login_access'])) { + $this->send_403($options, 'Access denied. Please use the correct login URL.'); + } + } + } + + public function redirect_filter(string $location, int $status): string { + $options = get_option('itk_security', []); + if (empty($options['enable_custom_login'])) return $location; + return str_replace('wp-login.php', $this->custom_slug($options), $location); + } + + public function network_url_filter(string $url, string $path): string { + $options = get_option('itk_security', []); + if (empty($options['enable_custom_login'])) return $url; + if (strpos($path, 'wp-login.php') !== false) { + return str_replace('wp-login.php', $this->custom_slug($options), $url); + } + return $url; + } + + public function site_url_filter(string $url, string $path): string { + $options = get_option('itk_security', []); + if (empty($options['enable_custom_login'])) return $url; + if (strpos($path, 'wp-login.php') !== false) { + return str_replace('wp-login.php', $this->custom_slug($options), $url); + } + return $url; + } + + /* ── File upload filter ───────────────────────────────────── */ + + public function filter_uploaded_files(array $file): array { + $options = get_option('itk_security', []); + if (empty($options['protect_uploads'])) return $file; + + if (preg_match('/\.(php|phtml|php\d|pht|exe|dll|asp|aspx|jsp|cgi|pl)$/i', $file['name'] ?? '')) { + $file['error'] = 'PHP and executable files cannot be uploaded.'; + } + return $file; + } + + /* ── Helpers ──────────────────────────────────────────────── */ + + private function send_403(array $options, string $message): void { + $code = $options['response_code'] ?? '403'; + $redir = $options['redirect_url'] ?? ''; + if ($code === '301_custom' && !empty($redir)) { + header('Location: ' . esc_url_raw($redir), true, 301); + } else { + status_header(403); + echo esc_html($message); + } + exit; + } + + private function custom_slug(array $options): string { + return !empty($options['custom_login_slug']) ? $options['custom_login_slug'] : 'thoushallpass'; + } + + private function load_allowed_ips(): void { + $defaults = ['127.0.0.1', '::1']; + if (file_exists($this->allowed_ips_file)) { + $lines = file($this->allowed_ips_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $line = trim($line); + if ($line !== '' && $line[0] !== '#') { + $this->allowed_ips[] = $line; + } + } + } + if (empty($this->allowed_ips)) { + $this->allowed_ips = $defaults; + } + } + + private function get_client_ip(): string { + $keys = [ + 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', + 'REMOTE_ADDR', + ]; + foreach ($keys as $key) { + if (empty($_SERVER[$key])) continue; + $ip = trim(explode(',', $_SERVER[$key])[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; + } + return 'UNKNOWN'; + } + + private function ip_in_cidr(string $ip, string $cidr): bool { + [$subnet, $mask] = explode('/', $cidr, 2); + if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; + if (!is_numeric($mask) || $mask < 0 || $mask > 32) return false; + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; + $mask_dec = ~((1 << (32 - (int)$mask)) - 1); + return (ip2long($ip) & $mask_dec) === (ip2long($subnet) & $mask_dec); + } + + public function get_allowed_ips(): array { return $this->allowed_ips; } + public function get_allowed_ips_file(): string { return $this->allowed_ips_file; } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..e71af0e --- /dev/null +++ b/index.php @@ -0,0 +1 @@ +Settings'); + return $links; + } + + public static function activate() { + ITK_Database::install(); + + // Default security settings + if (!get_option('itk_security')) { + add_option('itk_security', [ + 'block_openai_bots' => 1, + 'block_malicious_bots' => 1, + 'block_bad_referrers' => 1, + 'block_bad_networks' => 1, + 'rate_limit_good_bots' => 1, + 'protect_wp_login' => 1, + 'protect_wp_includes' => 1, + 'protect_uploads' => 1, + 'block_author_scans' => 1, + 'block_malicious_queries'=> 1, + 'add_security_headers' => 1, + 'block_xmlrpc' => 1, + 'enable_custom_login' => 0, + 'custom_login_slug' => 'thoushallpass', + 'response_code' => '301_custom', + 'redirect_url' => 'https://example.com/blocked', + 'custom_message' => 'Access denied.', + 'log_blocked_attempts' => 1, + ]); + } + + // Default optimization settings + if (!get_option('itk_optimization')) { + add_option('itk_optimization', [ + 'remove_wp_version' => 1, + 'hide_login_errors' => 1, + 'remove_author_class' => 1, + 'remove_script_versions' => 1, + 'change_author_base' => 1, + 'limit_revisions' => 1, + 'remove_emoji' => 1, + 'remove_default_userfields'=> 1, + 'clean_bad_content' => 1, + 'remove_wp_head_noise' => 1, + 'disable_xml_rpc' => 1, + 'deregister_wp_embed' => 1, + 'stop_empty_search_redirect'=> 1, + 'unregister_default_widgets'=> 1, + 'defer_js' => 1, + 'limit_heartbeat' => 1, + 'disable_dashboard_widgets'=> 1, + 'disable_comments_url' => 1, + 'disable_floc' => 1, + 'lightbox_images' => 1, + 'remove_admin_bar_links' => 1, + 'admin_branding' => 1, + 'use_google_jquery' => 0, + 'featured_image_rss' => 1, + 'dns_prefetch' => 1, + ]); + } + + // Default honeypot settings + if (!get_option('itk_honeypot')) { + add_option('itk_honeypot', [ + 'enabled' => 1, + 'protect_comments' => 1, + 'protect_login' => 1, + 'protect_register' => 1, + 'protect_lost_password'=> 1, + 'protect_woocommerce' => 1, + 'protect_cf7' => 1, + 'protect_elementor' => 1, + 'protect_gravity' => 1, + 'protect_search' => 1, + 'min_time' => 3, + 'max_time' => 7200, + 'retain_days' => 90, + ]); + } + + flush_rewrite_rules(); + } + + public static function deactivate() { + flush_rewrite_rules(); + } +} + +register_activation_hook(__FILE__, ['InformatiQ_Toolkit', 'activate']); +register_deactivation_hook(__FILE__, ['InformatiQ_Toolkit', 'deactivate']); + +add_action('plugins_loaded', ['InformatiQ_Toolkit', 'instance']); diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..27f579b --- /dev/null +++ b/uninstall.php @@ -0,0 +1,20 @@ +query("DROP TABLE IF EXISTS {$wpdb->prefix}itk_bot_log"); +$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}itk_honeypot_log");