Honeypot defense for WordPress

Every site has a back door.
Give them a fake one.

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.

  • PHP, no dependencies
  • Fail-open by design
  • No external services
01

The trap answers before WordPress loads the theme — so it sees the front end and wp-login.php alike.

The problem

Most bot traffic isn’t an attack. It’s a search.

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.

robots.txt is a map

A Disallow line points straight at the paths you care about. Scanners treat it as a to-do list.

Obvious doors get tried first

/wp-login.php. /xmlrpc.php. /.env. The same requests, from a rotating list of IPs, all day.

A miss is free

One 404 means nothing to a bot. It comes back with a new path and a new address — unless something notices.

How it works

Bait, catch, close the gate.

  1. A door that shouldn’t exist

    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.

    Regenerate the path anytime from the admin screen.

    # 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">
  2. A quiet catch

    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.

    No WAF rules, no heuristics, no substring guessing.

    // 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;
    }
  3. The gate stays shut

    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.

    Works for the front end and wp-login.php.

    # 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

Everything it does. Nothing it doesn’t.

The MVP is scoped like a tool, not a platform: one trap, one table of events, one settings page.

Exact matching

The trap path is compared as a whole string. No strpos, no false positives.

Two kinds of bait

A robots.txt line and a hidden footer link with nofollow, aria-hidden and tabindex="-1".

Three responses

404 by default, 403 for a hard refusal, or a configurable two-second tarpit.

Ban on first hit

Enabled out of the box. REMOTE_ADDR is the only source of truth — headers are not trusted.

Fail-open by design

If anything in the plugin breaks, your site keeps serving. Protection should never be the outage.

Privacy-minded logs

IP, path, method, status, user agent, referer. No request bodies, cookies, passwords or full headers.

Caching aware

The trap is excluded from speculative loading and page-cache paths where the cache supports it.

One admin page

Status, current path, regeneration, recent hits, ban list, cleanup — all under Settings → Honeypot.

Small on purpose

A plugin you can read in an afternoon.

No WAF, no malware scanner, no cloud console, no 2FA, no decoy maze. Those can be separate tools. This is one sharp one.

Lines of PHP
≈ 500–700
Database tables
2
Admin pages
1
External APIs
0

FAQ

Straight answers.

Will it block real visitors?

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.

Is this a WAF?

No. It blocks exactly one thing: requests to a path that nobody should know. It does not inspect payloads, signatures or user-agent lists.

What exactly gets logged?

Timestamp, IP, requested path, HTTP method, response status, user agent and referer. Both strings are truncated. Request bodies and headers are never stored.

Does it work with page caching?

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.

Multisite? A control panel? A cloud?

None of those in the MVP. One site, one database, one settings page, no outbound connections.

How do I get it?

The plugin is in early access. Write to [email protected] and tell me what you’re running.

Let them knock on the wrong door.

Early access is open. No sign-up forms, no trackers — just an email.

Request early access