Why WordPress bots keep knocking on your Java server
Stop wasting JVM threads on WordPress bots. Here is how to block PHP scanners at the Cloudflare, Nginx, and Spring Security levels to keep your Java logs clean.
Running a server online comes with a lot of risks. Without proper security, unauthorized users can access sensitive endpoints or disrupt services. In this post, we’ll walk through a simple practical example of basic protection of a Java server (like this blog) and show how to implement a basic anti-bot protection strategy.

How to block bots and scanners that are looking for WordPress on your Java servers
If your Spring Boot application has never run PHP, never installed WordPress, and never will — but your access logs are still full of requests for wp-login.php — congratulations.
You’re not under attack. You’re just on the internet.
Let’s talk about why this happens, when it matters, and how to deal with it the right way — without wasting JVM threads, flooding your logs, or turning your app into a bot-trolling science project.
---
Why bots look for WordPress on Java servers
Most WordPress scanners are aggressively dumb by design.
They don’t:
- Detect your tech stack
- Read your HTML
- Learn from failures
They do:
- Sweep IP ranges
- Try the same list of paths everywhere
- Move on immediately if nothing responds
To them, your Java server is just another door handle to jiggle.
---
The first rule: don’t fight bots in your controllers
If a bot request reaches your Spring controller:
- A JVM thread woke up
- Your app did work
- Your logs got noisier
That’s already more attention than the request deserved.
Yes, you can handle scanners in Spring Boot — with virtual threads, async handlers, or clever routing — but in production, that should be your last line of defense, not the first.
---
The right place to solve this: Nginx (if you don't have a WAF)
If your app doesn’t serve PHP, then any request ending in .php is invalid.
You don’t need a list of WordPress paths. You don’t need bot detection. You just need one rule.
Block All .php Requests with 410 Gone
# Java app, no PHP here
location ~* \.php$ {
return 410;
}That’s it.
- No JVM involvement
- No application logs
- No bandwidth wasted past the proxy
Why 410 instead of 404?
- It’s explicit
- It’s cacheable
- Scanners lose interest faster
---
Also block common “secret” files
Bots love guessing config files too.
location ~* ^/(\.env|\.git|\.svn|phpinfo\.php)$ {
return 410;
}Again: fast, boring, effective.
---
Keep this noise out of your access Logs
There’s no reason to log requests you’ve already decided to ignore.
map $request_uri $loggable {
default 1;
~*\.php$ 0;
~*^/(\.env|\.git|\.svn|phpinfo\.php)$ 0;
}
access_log /var/log/nginx/access.log combined if=$loggable;Now your logs contain things worth reading.
---
Optional: slow down everything else
If a scanner starts guessing paths instead of filenames, rate limiting makes it pointless.
limit_req_zone $binary_remote_addr zone=botlimit:10m rate=5r/s;
server {
location / {
limit_req zone=botlimit burst=10 nodelay;
proxy_pass http://spring_boot_upstream;
}
}This protects your app without affecting normal users.
---
What About Handling This in Java?
Yes, you can, this could be required if you don't have a proxy or you don't use a solution like Cloudflare.
- Java 21 virtual threads make tarpitting safer
- Async handlers prevent thread exhaustion
- Spring Security can short-circuit fake paths
These are useful tools — but they’re best reserved for defense in depth, not primary protection.
If Nginx can answer the request, your JVM shouldn’t even know it happened.
Spring Security Fallback: Defense in Depth
If you’re not using Nginx (or want an extra layer of protection), you can block .php requests directly in Spring Security. This is useful for:
Apps running behind proxies you don’t control Cloud environments where edge rules are limited Extra peace of mind.
``` java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.http.HttpStatus;
@Configuration public class BotBlockerConfig {
@Bean public SecurityFilterChain botBlockerFilterChain(HttpSecurity http) throws Exception { // Match all .php requests and common scanner paths RequestMatcher phpMatcher = new AntPathRequestMatcher("/*/.php"); RequestMatcher envMatcher = new AntPathRequestMatcher("/.env"); RequestMatcher gitMatcher = new AntPathRequestMatcher("/.git/"); RequestMatcher svnMatcher = new AntPathRequestMatcher("/.svn/"); RequestMatcher phpInfoMatcher = new AntPathRequestMatcher("/phpinfo.php");
RequestMatcher blockedRequests = new OrRequestMatcher( phpMatcher, envMatcher, gitMatcher, svnMatcher, phpInfoMatcher );
http .securityMatcher(blockedRequests) .authorizeHttpRequests(auth -> auth .requestMatchers(blockedRequests).denyAll() ) // Return 410 Gone for blocked requests .exceptionHandling(handling -> handling .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpStatus.GONE.value()); }) );
return http.build(); } }