June 24, 2026
I gave away free WordPress hosting. The phishers showed up almost immediately.
The whole point of wp.run is speed: type nothing, click once, and a few seconds later you have a real WordPress site on a real public…

By Armen
3 min read
The whole point of wp.run is speed: type nothing, click once, and a few seconds later you have a real WordPress site on a real public subdomain. No signup. Two hours later it's gone.
Perfect for testing a plugin or showing a client a demo.
Also perfect, it turns out, for stealing passwords.
Think about what I'd actually built from an attacker's point of view: free, instant, disposable infrastructure on a clean-looking domain that self-destructs in two hours — right before anyone gets around to reporting it. I'd built the ideal launchpad for a phishing kit and put a big "Create site" button on it.
So the question that blocked everything else was simple to ask and annoying to answer:
How do you teach a machine to tell a phishing page from an honest one?
"Password"? It's literally in wp-login. "Amazon"? It's in half the legitimate content on the internet. A login form? Not a crime. Every obvious signal, on its own, is just noise. Block on any of them and the only thing you reliably catch is your own users.
The thing that finally clicked: a phishing page isn't suspicious because of any one thing it does. It's suspicious because of the company those things keep.
So I stopped scoring features and started scoring combinations. The rule engine I wrote is deliberately almost useless one signal at a time:
→ a password field on its own? A low score. Just shrug.
→ a form that posts to someone else's server? Now I'm paying attention.
→ …wrapped in "PayPal" and "verify your account immediately"? Questions arise.
→ …and the whole thing quietly ships what you type to a collection endpoint on infrastructure that has nothing to do with the site you're looking at? That's not a website — that's a phishing kit.
No single rule can get you blocked. The score only crosses the line when the signals stack the exact way a real attack stacks them — credential form plus off-site exfil plus brand bait. Same trick catches card skimmers. False positives drop through the floor.
Then the kits taught me something I hadn't planned for.
Two calls I'd make again:
A lot of them weren't live. They were sitting in the container as files — staged, uploaded, not yet linked from the homepage. Scanning the served HTML would have missed them completely. So the scanner stopped being polite and started reaching into each site's container over the Docker API to triage the files on disk directly. Now I catch the kit while it's still loading the gun, not after it's fired.
By the time I hear about it, it's already over. The score crosses the line, the container dies, and only then does my phone buzz — the site, the score, every rule that fired, the URLs it was hiding, all laid out like an autopsy. A Delete button sits in the alert in case I want to finish the job by hand, but the job's usually done. I'm not catching attacks in the act. I'm reading their receipts.
Deterministic core, LLM on the margin.
The rule engine is intentionally dumb and predictable. Clear-cut cases get auto-actioned. Only the borderline scores get handed to an LLM reviewer. Every call has to be legible. "The model felt it was bad" is not something you can stand behind. "Password form + external POST + brand keywords" is. Cheap, fast, and explainable where it matters. You can always answer "why did this get flagged?"
The lesson travels well beyond WordPress: in abuse detection, the signal isn't any one feature — it's the correlation between features. Tune each rule to be nearly worthless on its own, and let the combinations do the deciding. You get far fewer false positives, and every block comes with a human-readable reason.