{"id":384036,"date":"2026-10-21T07:07:22","date_gmt":"2026-10-21T07:07:22","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/atv-web-security\/"},"modified":"2026-10-22T12:46:11","modified_gmt":"2026-10-22T12:46:11","slug":"atv-web-security","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/atv-web-security\/","author":23554947,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.38","stable_tag":"1.0.38","tested":"7.1.3","requires":"6.0","requires_php":"8.0","requires_plugins":null,"header_name":"ATV Web Security","header_author":"Arun Thomas","header_description":"Hardened multi-layer WordPress protection. Blocks unauthorized persistent changes to files, database, plugins, themes, configuration and users while normal visitors continue to use the website.","assets_banners_color":"013b7b","last_updated":"2026-10-22 12:46:11","external_support_url":"","external_repository_url":"","donate_link":"https:\/\/shop.atvsoftwarelabs.com\/product\/support-us\/","header_plugin_uri":"","header_author_uri":"https:\/\/www.atvsoftwarelabs.com\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":108,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.37":{"tag":"1.0.37","author":"atvsoftwarelabs","date":"2026-10-22 07:14:04","revision":3759456},"1.0.38":{"tag":"1.0.38","author":"atvsoftwarelabs","date":"2026-10-22 12:46:11","revision":3759922}},"upgrade_notice":{"1.0.37":"<ul>\n<li>Maintenance release for WordPress.org review compliance. No behaviour changes; updating is safe.<\/li>\n<li>If you already use the two-factor email code, nothing changes: the code field stays inline on the login form.<\/li>\n<\/ul>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3757882,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3757882,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3757882,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3757882,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.37","1.0.38"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[6459,1184,1178,600,9217],"plugin_category":[54],"plugin_contributors":[277671],"plugin_business_model":[],"class_list":["post-384036","plugin","type-plugin","status-publish","hentry","plugin_tags-lockdown","plugin_tags-malware","plugin_tags-protection","plugin_tags-security","plugin_tags-two-factor","plugin_category-security-and-spam-protection","plugin_contributors-atvsoftwarelabs","plugin_committers-atvsoftwarelabs"],"banners":{"banner":"https:\/\/ps.w.org\/atv-web-security\/assets\/banner-772x250.png?rev=3757882","banner_2x":"https:\/\/ps.w.org\/atv-web-security\/assets\/banner-1544x500.png?rev=3757882","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/atv-web-security\/assets\/icon-128x128.png?rev=3757882","icon_2x":"https:\/\/ps.w.org\/atv-web-security\/assets\/icon-256x256.png?rev=3757882","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>ATV Web Security puts your WordPress website into a \"protected\" state where visitors can still read and use the site, but nothing can persist changes to it \u2014 not plugins, not compromised administrators, not direct database access, and not a PHP shell.<\/p>\n\n<p>How it works \u2014 multiple independent layers:<\/p>\n\n<ul>\n<li>WordPress protection \u2014 plugin\/theme\/core operations, file editors, user creation, option writes and administrator actions are intercepted and denied.<\/li>\n<li>Database protection \u2014 the database account's write privileges are REVOKED at the MySQL\/MariaDB server level using a separate privileged MySQL account (allowed flows are re-granted only row-level INSERT\/UPDATE\/DELETE), so even direct SQL from arbitrary code is refused. The site account's own GRANT authority is never consumed, because revoking it would permanently destroy the restore path. An application-layer SQL firewall blocks write queries even when privileges cannot be changed.<\/li>\n<li>Filesystem protection \u2014 write bits are stripped from wp-admin, wp-includes, plugins, themes, mu-plugins, the plugin itself and critical config files. Original permissions are snapshotted and restored on disable.<\/li>\n<li>Upload protection \u2014 executable uploads are blocked by extension, MIME and content; the uploads directory is hardened so scripts cannot execute there.<\/li>\n<li>API protection \u2014 REST, AJAX, XML-RPC, WP-CLI and cron-based mutation is blocked. Reads remain available.<\/li>\n<li>Integration with the PHP firewall \u2014 traffic under wp-includes is rejected by a small mu-plugin, links to private files are blocked, and low-level PHP tricks are neutralised.<\/li>\n<li>Integrity monitoring \u2014 baseline hashes of core, configuration and plugin files are verified periodically.<\/li>\n<li>Access rules (optional) \u2014 AbuseIPDB-powered website access block by IP reputation score and a login country rule that only allows the admin login page from a country you select. Both are off by default, respect an IP\/CIDR allowlist, fail open on lookup errors and cache results to stay within API rate limits.<\/li>\n<li>Backend lock \u2014 the plugin lives in its own top-level admin menu; once protection is ON and a master password is set, the settings page stays hidden behind a lock screen until that password is entered (12-hour unlock, forged-cookie safe, brute-force rate limited). The master password accepts 8-64 letters, digits and symbols, and is the single password accepted to turn protection OFF (older 12-digit passwords keep working).<\/li>\n<li>Email verification code (Two-Factor, optional) \u2014 administrators can be asked for a six-digit code sent to their email address in addition to their WordPress password. The code field appears on the login form itself, right after the password is accepted, so the whole flow stays on one screen with no page refresh. Codes are valid for 10 minutes, can be resent once a minute (five codes per quarter hour), and the account locks for 15 minutes after five wrong entries. If the mailer cannot deliver, the login stays blocked on purpose \u2014 recovery is a single command from the shell: wp atvws otp disable.<\/li>\n<li>Passkeys \/ WebAuthn (optional, recommended for \"only my device\") \u2014 registers a hardware-backed credential with Windows Hello \/ TPM, Touch ID, a phone or a security key. The private key is created and used inside the device and never reaches the website, a cookie, JavaScript or a backup. With enforcement on, protected administrator logins are accepted only from a device whose passkey verifies; an account with no passkey is never blocked, and recovery stays available. An account covered by a registered passkey skips the emailed code \u2014 either factor alone satisfies the login.<\/li>\n<li>Trusted device lock (optional) \u2014 the portable fallback for browsers without WebAuthn. Binds protected actions to one computer with a one-time token in a secure HttpOnly, SameSite cookie. Any other computer gets a \"link your device\" screen that requires the master password, and linking\/unlinking always requires it. The Recovery tab stays reachable so a lost device can never lock the owner out permanently. A verified passkey also satisfies this gate.<\/li>\n<li>Behavior policy (optional) \u2014 studies privileged activity and locks the session instantly when the risk score crosses a threshold: failed unlock (15), failed disable attempt (25), failed restore attempt (20), failed login (20), refused passkey login (10). Scores are windowed per source IP for 24 hours; the owner (a linked-device or verified passkey session, or an allowlisted IP) is never scored and can clear a lock with the master password.<\/li>\n<li>Cryptomining defence \u2014 four independent layers: every rendered page is checked before it leaves PHP and script tags naming known mining services or pools are removed; WordPress's own HTTP API is refused for pool domains; a time-sliced filesystem scan walks wp-content and reports native executables and miner configuration markers; and a database scan searches posts and options for injected script blocks. The scans are report-only and surface on the Miners tab \u2014 nothing is quarantined by a heuristic match.<\/li>\n<li>Detection &amp; monitoring \u2014 a signed baseline of every script and stylesheet the login page loads, with a report the moment a new address appears (login page watch); a scheduled self-probe that asks the site for risky paths (backups, dumps, dotfiles, debug logs) and flags anything the web server would hand to anyone (sensitive file probe); honeypot decoy paths and hidden form fields that trigger escalating, temporary bans for automated scanners; and a local vulnerability inventory that flags plugins and themes that are outdated, abandoned (no release for 2+ years) or no longer listed in the wordpress.org API \u2014 computed locally, without phoning home.<\/li>\n<li>Snapshot backup and restore \u2014 a signed copy of every file WordPress can change (core, plugins, themes, uploads, wp-config.php) plus a complete database dump. A snapshot is captured automatically the first time protection is enabled; that \"current\" snapshot is what the documented manual recovery procedure uses. You can also keep as many named snapshots as you like (for example before a plugin or theme update) plus one automatic snapshot every day, each independently verifiable, restorable and deletable from the Snapshot tab or WP-CLI. There is no retention limit and nothing is ever deleted automatically \u2014 snapshots stay until you delete them yourself.<\/li>\n<li>Live progress \u2014 snapshot create, snapshot restore and integrity scans show a progress bar the moment you press the button, then fill in with stage, items processed and percentage while the operation runs.<\/li>\n<li>Total protection score \u2014 one honest number out of 100, shown on the Dashboard and in the header badge on every screen. Each security setting is worth a fixed number of points and only earns them once it is verified active, so the score drops the moment a layer stops working. The Dashboard lists every setting that is still pending, how many points it is worth, and which tab fixes it.<\/li>\n<li>Setup tour \u2014 the first time you open the plugin, a guided walkthrough takes you through the tabs in order: Diagnostics, Set protection level, Snapshot, then the rest of the configuration screens.<\/li>\n<li>Clear layout \u2014 every screen is grouped into Everyday, Configure, Check and Incident, with a live protection badge, plain-language explanations of what each button does, and destructive actions visually separated from safe ones.<\/li>\n<li>Recoverable \u2014 a forgotten master password no longer locks you out of the settings. Reset it from the plugin's runtime directory in uploads with cPanel\/FTP (create emergency-reset.key, then open emergency-reset.php), or from the shell with wp atvws password reset. Both routes sign out every session and are logged. WordPress account passwords can only be reset from WP-CLI. Email-code settings can be switched off from the shell with wp atvws otp disable.<\/li>\n<\/ul>\n\n<p>Protection tiers:<\/p>\n\n<ul>\n<li>MEDIUM \u2014 every PHP-level layer: WordPress, REST, AJAX, XML-RPC, upload, PHP shielding, integrity and CLI, plus the SQL firewall. Database grants and file permissions are left untouched, so it cannot lock you out of your own files and it runs on any host.<\/li>\n<li>STRONG \u2014 everything in Medium, plus the database account loses its write grants AND the protected files are made read-only. Both must verify active.<\/li>\n<li>HARD \u2014 everything in Strong, plus file changes are checked every hour instead of every six, and the tier does not count as fully in place until a verified file baseline is stored.<\/li>\n<li>LIMITED \u2014 protection is on but a core layer is switched off or did not verify after activation; the dashboard table shows which one before you rely on it.<\/li>\n<\/ul>\n\n<p>Strong and Hard require a separate privileged MySQL account (the wizard or the ATVWS_DB_ADMIN_* constants); without it those cards are shown as unavailable rather than silently downgraded, and the site's own GRANT authority is never consumed.<\/p>\n\n<p>Choosing or changing a tier requires the master password, so a hijacked admin session cannot quietly weaken the site. Every layer also has its own switch for fine-tuning a tier; picking a new tier resets those switches. The old names STANDARD, AUTO and HARDENED still work and map to Medium and Strong.<\/p>\n\n<p>Allowed writes:<\/p>\n\n<p>Persistent visitor flows (comments, contact forms, WooCommerce, user registration) are BLOCKED by default. The owner re-enables only what the site genuinely needs from the \"Allowed Writes\" page. Everything else stays blocked.<\/p>\n\n<p>Turning protection OFF is deliberately difficult (a strong request): you must submit a form, confirm a checkbox, type the exact phrase, and re-enter your account password (plus an optional passphrase). Emergency recovery paths (WP-CLI, a wp-config constant, a recovery file, or manual panel instructions) are documented to make sure you can always regain write access \u2014 see docs\/RECOVERY.md.<\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin can connect to the AbuseIPDB API. This is optional and off by default; it is used only for the IP intelligence features (the site-wide IP reputation block and the login country check), and only when you switch those features on.<\/p>\n\n<p>What the service is and what it is used for:<\/p>\n\n<ul>\n<li>AbuseIPDB provides an IP reputation and geolocation look-up. The plugin sends an IP address to AbuseIPDB to learn whether that address has been reported for abusive behaviour (a reputation score) and which country it is in, so the plugin can block abusive visitors or restrict administrator logins by country. No result is required for the site to work: if the lookup fails, the request is allowed (fail-open) and the failure is logged.<\/li>\n<\/ul>\n\n<p>What data is sent and when:<\/p>\n\n<ul>\n<li>The visitor's IP address is sent to AbuseIPDB only at the moment a protected request is checked by one of the IP intelligence features above, together with your own AbuseIPDB API key (in the request header) needed to authenticate the lookup.<\/li>\n<li>No other data is sent. No personal data, no site content, no account information and no usage statistics leave the site. Requests are rate-limited, cached, and only made while the feature is enabled.<\/li>\n<\/ul>\n\n<p>This service is provided by \"AbuseIPDB LLC\". Terms of use, privacy policy.<\/p>\n\n<ul>\n<li>Terms of use: https:\/\/www.abuseipdb.com\/legal<\/li>\n<li>Privacy policy: https:\/\/www.abuseipdb.com\/privacy<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>atv-web-security<\/code> folder to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin through the Plugins screen.<\/li>\n<li>Under the new top-level \"ATV Web Security\" admin menu, follow the setup tour: review the Diagnostics tab, then click \"Turn ON Protection\".<\/li>\n<li>Store the recovery token that is displayed once.<\/li>\n<li>Configure \"Allowed Writes\", \"Protected Admins\" and \"Two-Factor\" to match your site.<\/li>\n<\/ol>\n\n<p>More detail: docs\/INSTALL.md and docs\/CONFIGURATION.md.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"can%20visitors%20still%20read%20the%20site%3F\"><h3>Can visitors still read the site?<\/h3><\/dt>\n<dd><p>Yes. The site remains fully readable and functional for browsing. Only persistent changes are blocked.<\/p><\/dd>\n<dt id=\"what%20happens%20while%20protection%20is%20on%3F\"><h3>What happens while protection is ON?<\/h3><\/dt>\n<dd><p>Users (including administrators) can browse, read and perform allowed writes. Plugin, theme, core updates, file and database changes that are not explicitly allowlisted are blocked. Turn protection OFF to build, update or modify anything.<\/p><\/dd>\n<dt id=\"how%20do%20i%20turn%20protection%20off%3F\"><h3>How do I turn protection OFF?<\/h3><\/dt>\n<dd><p>Use the Dashboard tab and follow the strong-disable form, or use one of the emergency recovery methods described in docs\/RECOVERY.md.<\/p><\/dd>\n<dt id=\"which%20password%20is%20accepted%20to%20turn%20protection%20off%3F\"><h3>Which password is accepted to turn protection OFF?<\/h3><\/dt>\n<dd><p>The master password (8-64 letters, digits and symbols) is accepted, and so is your WordPress account password as a fallback \u2014 so a site that never configured a master password is never locked out.<\/p><\/dd>\n<dt id=\"how%20does%20the%20emailed%20verification%20code%20work%3F\"><h3>How does the emailed verification code work?<\/h3><\/dt>\n<dd><p>When enabled, administrators are asked for a six-digit code sent to their email address after their password is accepted. The code field appears on the login form itself, so no page refresh is needed and every step stays on one screen. A code is valid for 10 minutes, can be resent once a minute (five codes per quarter hour), and the account locks for 15 minutes after five wrong entries.<\/p><\/dd>\n<dt id=\"what%20if%20i%20cannot%20receive%20the%20emailed%20code%3F\"><h3>What if I cannot receive the emailed code?<\/h3><\/dt>\n<dd><p>The login stays blocked on purpose rather than silently downgrading to password-only. Recover from the shell with wp atvws otp disable (or re-enter the master password on an already-unlocked session).<\/p><\/dd>\n<dt id=\"passkey%20or%20email%20code%20%E2%80%94%20which%20do%20i%20need%3F\"><h3>Passkey or email code \u2014 which do I need?<\/h3><\/dt>\n<dd><p>Either factor alone satisfies the check. An account covered by a registered passkey skips the emailed code, and the passkey is hardware-bound so it survives cookie clearing and cannot be copied. Use the two-factor email code when a browser cannot do WebAuthn.<\/p><\/dd>\n<dt id=\"do%20i%20need%20a%20passkey%2C%20or%20is%20the%20device%20token%20enough%3F\"><h3>Do I need a passkey, or is the device token enough?<\/h3><\/dt>\n<dd><p>The device token is bound to the browser profile: clearing cookies or switching browsers unlinks it, and a copied cookie could be replayed. A passkey is bound to the device's secure hardware, so it survives cookie clearing and cannot be copied. If you can only pick one, pick the passkey; keep the token as the fallback for browsers that cannot do WebAuthn. Enforcement is off until you turn it on, and no account is ever blocked without a registered passkey.<\/p><\/dd>\n<dt id=\"what%20does%20the%20miners%20tab%20do%3F\"><h3>What does the Miners tab do?<\/h3><\/dt>\n<dd><p>It reports cryptomining findings without touching anything: script tags naming mining services or pools are removed from rendered pages, WordPress's HTTP API is refused for pool domains, a filesystem scan flags native executables and miner configuration markers, and a database scan looks for injected script blocks. Every result is surfaced on the Miners tab for you to review and act on.<\/p><\/dd>\n<dt id=\"how%20does%20the%20vulnerability%20checker%20decide%20what%20is%20abandoned%3F\"><h3>How does the vulnerability checker decide what is abandoned?<\/h3><\/dt>\n<dd><p>It measures locally, with no third-party call: a release older than two years is flagged as unmaintained, a plugin or theme still on disk but no longer listed in the wordpress.org API is flagged as closed, and the local modification time of the plugin directory cross-checks both. An online feed lookup exists but is opt-in and requires your own API key \u2014 nothing phones home about the site by default.<\/p><\/dd>\n<dt id=\"is%20my%20database%20really%20read-only%3F\"><h3>Is my database really read-only?<\/h3><\/dt>\n<dd><p>On Strong and Hard Protection a separate privileged MySQL account revokes the database account's write privileges and the result is verified. On Medium Protection the SQL firewall blocks write queries from PHP instead. Details in docs\/INSTALL.md and docs\/SECURITY_LIMITATIONS.md.<\/p><\/dd>\n<dt id=\"what%20about%20caching%20plugins%3F\"><h3>What about caching plugins?<\/h3><\/dt>\n<dd><p>Cache plugins are confined to an explicit allowlist of cache directories; other paths stay read-only. See diagnostics for the ongoing compatibility status.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.37<\/h4>\n\n<ul>\n<li>Maintenance release addressing WordPress.org plugin review feedback. No feature or behaviour changes.<\/li>\n<li>Admin stylesheet is now loaded with wp_enqueue_style instead of a printed raw style block.<\/li>\n<li>Theme and plugin headers are read with get_file_data() instead of file_get_contents().<\/li>\n<li>The AbuseIPDB external service is now documented, with links to its terms and privacy policy.<\/li>\n<li>Runtime, state and recovery paths now use wp_upload_dir() and WPMU_PLUGIN_DIR for custom and multisite layouts.<\/li>\n<li>Removed the global automatic-update filters; automatic updates are left to WordPress.<\/li>\n<li>Removed the automatic mu-plugin installer; the optional MU bootstrap is a manual copy.<\/li>\n<li>The miner-guard output buffer is now always closed.<\/li>\n<li>The top-level admin menu is placed lower (position 100).<\/li>\n<li>Remaining request inputs are sanitised and validated; output is escaped.<\/li>\n<\/ul>\n\n<h4>1.0.36<\/h4>\n\n<ul>\n<li>Email verification code now appears as an inline field on the login form \u2014 no page refresh, the whole two-factor flow stays on one screen.<\/li>\n<li>Resend is throttled to once a minute and five codes per quarter hour; a failed delivery keeps the login blocked on purpose so the mailer cannot fail open.<\/li>\n<li>Codes are probed in the background on submit, with a clear error on the form when the code is wrong.<\/li>\n<\/ul>\n\n<h4>1.0.35<\/h4>\n\n<ul>\n<li>Added email verification codes (Two-Factor): administrators can be required to enter a six-digit code emailed to them after the account password. Passkey-covered accounts skip the code \u2014 either factor alone satisfies the login.<\/li>\n<\/ul>\n\n<h4>1.0.34<\/h4>\n\n<ul>\n<li>Allowed writes now defer to the SQL firewall's enforced rule server-side, so a server-level write block is verified against the same rule that actually runs.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<\/ul>","raw_excerpt":"Remove a compromised website's ability to make persistent changes while keeping it fully readable to visitors.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/384036","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=384036"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/atvsoftwarelabs"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=384036"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=384036"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=384036"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=384036"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=384036"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=384036"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}