robots.txt is a map
A Disallow line points straight at the paths you care about. Scanners treat it as a to-do list.
Honeypot defense for WordPress
UnAttack is a deliberately small honeypot: a hidden trap path, a decoy
in robots.txt, and an IP ban on the first hit — before a
scanner ever reaches your login.
The trap answers before WordPress loads the theme — so it sees the front end and wp-login.php alike.
The problem
Scanners read robots.txt to find what you tried to hide, then
walk the obvious paths. One miss costs them nothing, and they never get tired.
A Disallow line points straight at the paths you care about. Scanners treat it as a to-do list.
/wp-login.php. /xmlrpc.php. /.env. The same requests, from a rotating list of IPs, all day.
One 404 means nothing to a bot. It comes back with a new path and a new address — unless something notices.
How it works
Activation generates /hp- plus sixteen random characters.
The only ways in are an invisible footer link and a line in
robots.txt — nothing a human can reach.
# generated on activation
/hp-9f3c1d7a2b8e4c60/
# robots.txt
User-agent: *
Disallow: /hp-9f3c1d7a2b8e4c60/
# footer, hidden from humans
<a href="/hp-…/" style="display:none"
rel="nofollow noopener"
aria-hidden="true" tabindex="-1">
The request is matched exactly on init, priority 0 — before
the theme renders. You pick the answer: a neutral 404, a hard
403, or a two-second tarpit.
// init, priority 0
$path = normalize($_SERVER['REQUEST_URI']);
if ($path === $settings['slug']) {
$status = $settings['response']; // 404
log_hit($ip, $path, $method);
if ($settings['ban']) ban($ip);
exit;
}
By default the first hit bans the address for 24 hours. Choose one hour, one week or permanent. Trusted IPs and CIDR ranges are never logged or blocked, and a cron job clears expired bans.
# hp_bans
ip 45.155.205.233
reason trap hit
created_at 2026-10-22 14:02:08
expires_at 2026-10-23 14:02:08
hits 1
# cleanup
wp cron → purge_expired_bans()
Features
The MVP is scoped like a tool, not a platform: one trap, one table of events, one settings page.
The trap path is compared as a whole string. No strpos, no false positives.
A robots.txt line and a hidden footer link with nofollow, aria-hidden and tabindex="-1".
404 by default, 403 for a hard refusal, or a configurable two-second tarpit.
Enabled out of the box. REMOTE_ADDR is the only source of truth — headers are not trusted.
If anything in the plugin breaks, your site keeps serving. Protection should never be the outage.
IP, path, method, status, user agent, referer. No request bodies, cookies, passwords or full headers.
The trap is excluded from speculative loading and page-cache paths where the cache supports it.
Status, current path, regeneration, recent hits, ban list, cleanup — all under Settings → Honeypot.
Small on purpose
No WAF, no malware scanner, no cloud console, no 2FA, no decoy maze. Those can be separate tools. This is one sharp one.
FAQ
A person would have to click a link that is display:none,
unreachable by keyboard and hidden from screen readers. Crawlers follow
it anyway. If someone is blocked by mistake, add them to the IP or CIDR
whitelist.
No. It blocks exactly one thing: requests to a path that nobody should know. It does not inspect payloads, signatures or user-agent lists.
Timestamp, IP, requested path, HTTP method, response status, user agent and referer. Both strings are truncated. Request bodies and headers are never stored.
The trap is handled early and excluded from speculative loading; known cache plugins can be told to skip the path. The hidden footer link is not printed for logged-in users, REST, AJAX or the admin.
None of those in the MVP. One site, one database, one settings page, no outbound connections.
The plugin is in early access. Write to [email protected] and tell me what you’re running.
Early access is open. No sign-up forms, no trackers — just an email.