DEV Community

Cover image for Inside a WordPress Bot Detection Engine
webdecoy
webdecoy

Posted on Originally published at webdecoy.com

Inside a WordPress Bot Detection Engine

The WebDecoy WordPress plugin ships with zero configuration required. But underneath the "install, activate, done" experience is a multi-layer detection engine that scores every request across server-side signals, client-side fingerprints, behavioral analysis, and proof-of-work verification.

This post walks through how each layer works, how they combine into a single threat score, and why this architecture catches bots that simpler approaches miss. It's a WordPress plugin, but the scoring design applies to any request pipeline.

The Detection Pipeline

Every request flows through a pipeline that evaluates it before WordPress processes it:

Incoming Request
  │
  ├─ Is this IP blocked? → Yes → Block page
  │
  ├─ Is this a known good bot? → Verify via reverse DNS → Allow
  │
  ├─ Server-side analysis
  │    ├─ User-Agent patterns
  │    ├─ HTTP header consistency
  │    ├─ MITRE ATT&CK path matching
  │    └─ Rate limit check
  │
  ├─ Client-side signals (on form submission)
  │    ├─ WebDriver / headless detection
  │    ├─ Automation framework markers
  │    ├─ Canvas / WebGL fingerprint
  │    └─ Behavioral scoring
  │
  ├─ Proof-of-Work verification (on form submission)
  │    └─ SHA-256 challenge validation
  │
  └─ Score aggregation → Allow / Challenge / Block
Enter fullscreen mode Exit fullscreen mode

The first two checks are fast exits. Blocked IPs get rejected immediately. Verified good bots skip detection entirely. Everything else gets scored.

Threat Scoring: 0 to 100

Every detection signal adds points to a threat score. The score determines what happens to the request:

Score Range Severity Action
0–19 Minimal Allow (likely human)
20–39 Low Log only
40–59 Medium Optional challenge
60–74 High Challenge or block
75–100 Critical Automatic block

The default blocking threshold is 75, configurable in settings. Scores at 40 and above are logged for review.

The scoring is additive. A request doesn't need to fail one dramatic test — it accumulates evidence across multiple signals. A slightly suspicious user agent (+25) combined with missing cookies (+15) and an unusual request path (+20) adds up to 60, enough to trigger a challenge. No single signal is conclusive, but the combination tells a clear story.

Base scores for common signals:

Missing standard headers:      10-30
No cookies on non-first visit:    15
Suspicious user agent:            25
Known bot user agent:             50
curl / wget / python-requests:    35
Automation tool detected:         40
Headless browser markers:         25
Rate limit exceeded:              25
Honeypot field triggered:         60
Fake bot (failed DNS verify):     80
Enter fullscreen mode Exit fullscreen mode

A real Chrome browser hitting a normal page scores near zero. A Python script with a spoofed user agent, no cookies, and missing standard headers quickly crosses the blocking threshold.

Server-Side Analysis

User-Agent and header consistency

The plugin checks the User-Agent against known bot patterns (curl, wget, python-requests, Go-http-client, scrapy, and dozens more) and evaluates header consistency. Real browsers send a predictable set of headers — Accept, Accept-Language, Accept-Encoding, Connection — in a consistent order. Automated tools frequently omit headers or send them in unusual combinations.

Missing Accept-Language is a strong signal. Every real browser sends it. Most HTTP libraries don't unless explicitly configured.

MITRE ATT&CK path matching

This is one of the more distinctive pieces. Rather than maintaining an arbitrary blocklist of "bad" URLs, detection is organized by attacker tactic:

Credential Access (TA0006):
  .env, wp-config.php, .git/, *.sql         → +30 points

Collection (TA0009):
  Backup files, database dumps              → +25 points

Reconnaissance (TA0043):
  Admin probes, user enumeration            → +20 points

Discovery (TA0007):
  Debug endpoints, phpinfo, server-status   → +20 points
Enter fullscreen mode Exit fullscreen mode

When an IP requests /wp-config.php.bak, then /.env, then /.git/config, each request scores individually while the rate limiter tracks velocity. The combined effect is rapid escalation to the blocking threshold.

The mapping isn't only for scoring. It surfaces in the detections table, so you can see a blocked IP was performing credential access reconnaissance rather than just "requesting bad URLs." The categorization tells you what attackers are actually looking for.