XF Bot Guard

XF Bot Guard 1.4.1

No permission to download
And as a side note, these options do not exist, not sure where this is coming from.

I am probably just not guessing correctly at your version of XenForo, because I was not sent the support bundle. All I can do is guess at the moment.

Ideally the captcha should just be rendered through the xf:captcha template tag, or in the worst case through a View class which should have access to the correct templater object for the style being used

It does render through the XenForo captcha template. But there is an incompatibility doing it this way on 2.2, it seems. I have already, as mentioned, patched it now for the next release. So a long term fix is coming, and I am merely trying to help you with a now option while you wait.

If you don't want to do that, I understand - that is your choice, but I cannot rush out a full feature release at the moment.
 

A note before upgrading XenForo​


Just a general note for everyone using XF Bot Guard:

Bot Guard is not a small, isolated add-on. To provide the level of protection it does, it integrates deeply with XenForo and extends a number of core systems. That is entirely by design, but it also means XenForo upgrades need to be approached with a little more care.

For routine XenForo releases, I strongly recommend checking this support thread and waiting for my compatibility confirmation - or “all clear” - before upgrading a production site.

If you need to upgrade before I have confirmed compatibility, the safer approach is to:

  • Make sure you have a current backup.
  • Disable Bot Guard before upgrading.
  • Complete the XenForo upgrade.
  • Re-enable Bot Guard when you are ready to test it carefully.
  • Run the Bot Guard Health Check and watch for errors or unexpected behaviour.

Testing on a staging installation first is preferable wherever possible.

To be clear, this does not mean Bot Guard is doing anything it should not be doing.

Bot Guard legitimately uses XenForo’s extension system and integrates with a wide range of underlying functionality because that is necessary for the features it provides.

The difficulty is that XenForo point releases can alter internal classes, extension behaviour or other underlying systems that add-ons rely upon. These changes are not always obvious from the public-facing release notes, and an upgrade completing successfully does not necessarily mean every installed add-on remains fully compatible.

Because Bot Guard is a large, heavily integrated security product, I need to properly review and test each XenForo release before giving it my rubber stamp.

This is not advice to ignore urgent security updates. Where an immediate XenForo upgrade is necessary, disable Bot Guard, perform the upgrade and test carefully rather than simply leaving everything enabled and assuming compatibility.
 
Regarding the latest release, v2.3.12:

The release notes mention changes to class extensions, so this update will warrant a thorough compatibility review on my end, which I expect to complete this week.

I have also received user reports suggesting that v2.3.12 may cause issues with Original Survival Mode. Please be aware that there may be an incompatibility I need to address before the next add-on release.
 
I am probably just not guessing correctly at your version of XenForo, because I was not sent the support bundle. All I can do is guess at the moment.

It doesn't exist in any version as far as I'm aware 🤔

It does render through the XenForo captcha template. But there is an incompatibility doing it this way on 2.2, it seems. I have already, as mentioned, patched it now for the next release. So a long term fix is coming, and I am merely trying to help you with a now option while you wait.

Yeah, I'm saying it's rendering it using the master style instead of the correct selected theme, and is being done in a way that it happens before XenForo style properties are available. I do not believe this has anything to do with XenForo 2.2 vs 2.3, doing it with that method will still render it with the master style template rather than the one selected, the only difference would be that it would use the style property through CSS variables in 2.3. I'm just pointing out what is being done currently and why it's not working so it can be fixed in the appropriate way, I already have the patch applied to sites we're using it on with XF 2.2 :)
 
I had to uninstall this modification today. On a busy site this query was taking upwards of 500 seconds (it's a busy site, gets hammered a lot). Here is the query and the AI response, sorry for the formatting. I can't figure out how to export a good Claude chat session:

Code:
Why might this query be running very slow? EXPLAIN DELETE d FROM xf_zee_botguard_event_detail AS d INNER JOIN ( SELECT event_id FROM ( SELECT d2.event_id FROM xf_zee_botguard_event_detail AS d2 LEFT JOIN xf_zee_botguard_event AS e ON e.event_id = d2.event_id WHERE e.event_id IS NULL ORDER BY d2.event_id LIMIT 1000 ) AS pending_event_detail_orphans ) AS doomed ON doomed.event_id = d.event_id:  *** row 1 ***           table:  <derived2>            type:  ALL   possible_keys:  NULL             key:  NULL         key_len:  NULL             ref:  NULL            rows:  1000           Extra:  NULL *** row 2 ***           table:  d            type:  eq_ref   possible_keys:  PRIMARY             key:  PRIMARY         key_len:  4             ref:  doomed.event_id            rows:  1           Extra:  NULL *** row 3 ***           table:  <derived3>            type:  ALL   possible_keys:  NULL             key:  NULL         key_len:  NULL             ref:  NULL            rows:  1000           Extra:  NULL *** row 4 ***           table:  d2            type:  index   possible_keys:  NULL             key:  PRIMARY         key_len:  4             ref:  NULL            rows:  13569290           Extra:  Using index *** row 5 ***           table:  e            type:  eq_ref   possible_keys:  PRIMARY             key:  PRIMARY         key_len:  4             ref:  forum_xen.d2.event_id            rows:  1           Extra:  Using where; Using index; Not exists

Show more
Diagnosed quadratic complexity from repeated full table scans
Diagnosed quadratic complexity from repeated full table scans

A few things stack up here, but the dominant one is row 4:

table: d2   type: index   key: PRIMARY   rows: 13569290   Extra: Using index

That's a full scan of the detail table's clustered index — 13.5M rows — with an eq_ref probe into xf_zee_botguard_event for each one. The LIMIT 1000 doesn't bound that scan; it only stops it once 1000 rows have survived the e.event_id IS NULL filter. If orphans are rare, or clustered toward the high end of event_id, MySQL grinds through millions of rows and millions of random PK lookups before it collects its thousand.

The killer case is the tail end: once fewer than 1000 orphans remain, the query must read all 13.5M rows to prove it. Every "empty" run costs a full table scan.

If you're running this in a loop, it's quadratic. Each iteration restarts from the lowest event_id and re-walks all the non-orphan rows it already rejected last time. 13,569 batches × an ever-growing prefix scan is the real reason it feels like it never finishes.

Some secondary contributors:

The <derived2> / <derived3> double nesting is materialized into temp tables. You need one level (MySQL error 1093 — can't read the target table in a subquery of a DELETE), but the outer wrapper is redundant work.
13.5M random lookups into e's primary key is I/O-bound unless both clustered indexes fit in the buffer pool. Worth checking innodb_buffer_pool_size against the size of the two tables.
Any secondary indexes on the detail table make each row delete more expensive, though that's minor next to the scan.

The ORDER BY d2.event_id is actually free — it matches PK order, so no filesort.

The fix: range chunks instead of LIMIT

Multi-table DELETE doesn't support LIMIT, which is presumably why you built the derived-table scaffolding. But it does support a WHERE clause, so walk the key space instead:

sql
DELETE d
FROM xf_zee_botguard_event_detail AS d
LEFT JOIN xf_zee_botguard_event AS e ON e.event_id = d.event_id
WHERE e.event_id IS NULL
  AND d.event_id >= ? AND d.event_id < ?;

Drive ? from a cursor you advance by, say, 50,000 IDs per iteration. Each batch touches a bounded range of the clustered index, no derived tables, no re-scanning, and the total work across the whole job is a single pass over the table.

If you'd rather keep the LIMIT shape, the minimum change is to add AND d2.event_id > ? seeded with the highest event_id examined in the previous batch — not the highest deleted. That alone converts the O(n²) loop into O(n).

Third option if orphans are sparse: pay for one full scan up front, dump the orphan IDs into a work table, then delete in batches joined against that. One scan total instead of one per batch.
 
It doesn't exist in any version as far as I'm aware 🤔



Yeah, I'm saying it's rendering it using the master style instead of the correct selected theme, and is being done in a way that it happens before XenForo style properties are available. I do not believe this has anything to do with XenForo 2.2 vs 2.3, doing it with that method will still render it with the master style template rather than the one selected, the only difference would be that it would use the style property through CSS variables in 2.3. I'm just pointing out what is being done currently and why it's not working so it can be fixed in the appropriate way, I already have the patch applied to sites we're using it on with XF 2.2 :)

I’ve changed it so the controller now prepares the CAPTCHA object, but the actual CAPTCHA HTML is rendered in the view using the response renderer’s templater:

PHP:
$captcha->render($this->renderer->getTemplater());

So it’s still using XenForo’s CAPTCHA rendering, just at the point where the public renderer has the correct style and style-variation context.

This will be available in the next release.
 
so this is just a stupid suggestion that can be totally ignored lol. ability to whitelist certain forums that have junk data for bots to waste time on. i have a few sections that i would not mind ai bots spending their time on.
 
I had to uninstall this modification today. On a busy site this query was taking upwards of 500 seconds (it's a busy site, gets hammered a lot). Here is the query and the AI response, sorry for the formatting. I can't figure out how to export a good Claude chat session:

Code:
Why might this query be running very slow? EXPLAIN DELETE d FROM xf_zee_botguard_event_detail AS d INNER JOIN ( SELECT event_id FROM ( SELECT d2.event_id FROM xf_zee_botguard_event_detail AS d2 LEFT JOIN xf_zee_botguard_event AS e ON e.event_id = d2.event_id WHERE e.event_id IS NULL ORDER BY d2.event_id LIMIT 1000 ) AS pending_event_detail_orphans ) AS doomed ON doomed.event_id = d.event_id:  *** row 1 ***           table:  <derived2>            type:  ALL   possible_keys:  NULL             key:  NULL         key_len:  NULL             ref:  NULL            rows:  1000           Extra:  NULL *** row 2 ***           table:  d            type:  eq_ref   possible_keys:  PRIMARY             key:  PRIMARY         key_len:  4             ref:  doomed.event_id            rows:  1           Extra:  NULL *** row 3 ***           table:  <derived3>            type:  ALL   possible_keys:  NULL             key:  NULL         key_len:  NULL             ref:  NULL            rows:  1000           Extra:  NULL *** row 4 ***           table:  d2            type:  index   possible_keys:  NULL             key:  PRIMARY         key_len:  4             ref:  NULL            rows:  13569290           Extra:  Using index *** row 5 ***           table:  e            type:  eq_ref   possible_keys:  PRIMARY             key:  PRIMARY         key_len:  4             ref:  forum_xen.d2.event_id            rows:  1           Extra:  Using where; Using index; Not exists

Show more
Diagnosed quadratic complexity from repeated full table scans
Diagnosed quadratic complexity from repeated full table scans

A few things stack up here, but the dominant one is row 4:

table: d2   type: index   key: PRIMARY   rows: 13569290   Extra: Using index

That's a full scan of the detail table's clustered index — 13.5M rows — with an eq_ref probe into xf_zee_botguard_event for each one. The LIMIT 1000 doesn't bound that scan; it only stops it once 1000 rows have survived the e.event_id IS NULL filter. If orphans are rare, or clustered toward the high end of event_id, MySQL grinds through millions of rows and millions of random PK lookups before it collects its thousand.

The killer case is the tail end: once fewer than 1000 orphans remain, the query must read all 13.5M rows to prove it. Every "empty" run costs a full table scan.

If you're running this in a loop, it's quadratic. Each iteration restarts from the lowest event_id and re-walks all the non-orphan rows it already rejected last time. 13,569 batches × an ever-growing prefix scan is the real reason it feels like it never finishes.

Some secondary contributors:

The <derived2> / <derived3> double nesting is materialized into temp tables. You need one level (MySQL error 1093 — can't read the target table in a subquery of a DELETE), but the outer wrapper is redundant work.
13.5M random lookups into e's primary key is I/O-bound unless both clustered indexes fit in the buffer pool. Worth checking innodb_buffer_pool_size against the size of the two tables.
Any secondary indexes on the detail table make each row delete more expensive, though that's minor next to the scan.

The ORDER BY d2.event_id is actually free — it matches PK order, so no filesort.

The fix: range chunks instead of LIMIT

Multi-table DELETE doesn't support LIMIT, which is presumably why you built the derived-table scaffolding. But it does support a WHERE clause, so walk the key space instead:

sql
DELETE d
FROM xf_zee_botguard_event_detail AS d
LEFT JOIN xf_zee_botguard_event AS e ON e.event_id = d.event_id
WHERE e.event_id IS NULL
  AND d.event_id >= ? AND d.event_id < ?;

Drive ? from a cursor you advance by, say, 50,000 IDs per iteration. Each batch touches a bounded range of the clustered index, no derived tables, no re-scanning, and the total work across the whole job is a single pass over the table.

If you'd rather keep the LIMIT shape, the minimum change is to add AND d2.event_id > ? seeded with the highest event_id examined in the previous batch — not the highest deleted. That alone converts the O(n²) loop into O(n).

Third option if orphans are sparse: pay for one full scan up front, dump the orphan IDs into a work table, then delete in batches joined against that. One scan total instead of one per batch.

Thanks for the report - I will introduce a fix for this which will be available in the next release.
 
so this is just a stupid suggestion that can be totally ignored lol. ability to whitelist certain forums that have junk data for bots to waste time on. i have a few sections that i would not mind ai bots spending their time on.

Interesting - so, an excluded forum nodes kind of option?
 
yeah i mean it is not really something you should waste time on tbh. and i doubt others would be interested in since no one has asked for this so far. but i do think that it would be sort of fun to allow bots to access chitchat type forums that have no useful data for them. it is still wasting server resources on them though.
 
yeah i mean it is not really something you should waste time on tbh. and i doubt others would be interested in since no one has asked for this so far. but i do think that it would be sort of fun to allow bots to access chitchat type forums that have no useful data for them. it is still wasting server resources on them though.
Noted, thank you! On the backlog :)
 
yeah i mean it is not really something you should waste time on tbh. and i doubt others would be interested in since no one has asked for this so far. but i do think that it would be sort of fun to allow bots to access chitchat type forums that have no useful data for them. it is still wasting server resources on them though.

Cloudflare has an AI Labyrinth option to do just that. Probably works better... besides, AI hammering your forum section could still take down your server and waste resources.
 

XF Bot Guard Compatibility Report: XenForo 2.3.12​


XF Bot Guard has been reviewed against XenForo 2.3.12 and is cleared for compatibility.

A source comparison between XenForo 2.3.11 and 2.3.12, together with focused runtime testing, found no changes that interfere with XF Bot Guard’s:

  • Public request handling
  • XenForo class extensions
  • Database usage
  • Origin Survival Mode behaviour

No XF Bot Guard code changes are currently required to support XenForo 2.3.12.

Origin Survival Mode and 503 Responses​


Some customers have reported that Origin Survival Mode begins returning 503 Service Unavailable responses sooner than expected during or shortly after upgrading to XenForo 2.3.12.

This does not appear to be a XenForo 2.3.12 compatibility issue.

A 503 response is the intended behaviour of Origin Survival Mode when the feature is enabled and the configured origin-pressure thresholds are reached. XenForo upgrades can temporarily place significant additional load on the server and database due to:

  • Database upgrade queries
  • Cache rebuilds
  • Template and data rebuilds
  • Increased MySQL connections
  • Higher CPU usage and I/O wait
  • Other upgrade and maintenance operations

If this temporary load crosses the configured thresholds, XF Bot Guard may correctly enter survival mode and begin shedding unknown guest traffic to protect the origin server.

In other words, the 503 responses are not a failure of the responder. They indicate that the emergency protection is operating as designed. XenForo upgrades are simply a situation in which Origin Survival Mode may activate earlier than normally expected because the database and server are already performing intensive work.

Upgrade Recommendation​


For sites using Origin Survival Mode, I recommend using one of the following approaches when applying XenForo upgrades:

  1. Disable XF Bot Guard before beginning the upgrade.

    Re-enable it once the XenForo upgrade has completed and server and database load have returned to normal.

  2. Disable only Origin Survival Mode before beginning the upgrade.

    The remaining XF Bot Guard protections can stay enabled. Re-enable Origin Survival Mode once the upgrade has completed and the site is stable.

This is particularly recommended for larger forums, or on hosting environments where XenForo upgrade operations noticeably increase MySQL connections, CPU usage, I/O wait or general server pressure.
 
Cloudflare has an AI Labyrinth option to do just that. Probably works better... besides, AI hammering your forum section could still take down your server and waste resources.
yeah i have been trying to stop using cloudflare now. have been managing just fine for the last 2 months mostly because of this addon lol. cloudflare was actually making my site slow probably coz nodes in india are overloaded or not managed properly. plus cloudflare now has too much control over the web now lol. they should not be the requirement to run a website in 2026 lol. anyways.
 
Just one suggestion - could the 'docs' directory be removed from the web route? Seems like something that shouldn't be publicly accessible, and could easily be rendered on an admin page somewhere

Another potential bug I've come across - it doesn't seem to use the fs adapters for the mmdb file, I store internal data in an S3 bucket on several sites, but it's hardcoding it into an internal_data/zee directory, which in some cases is okay, but in the cases where we have multiple webservers it's leading to inconsistencies because different webservers are ending up with different versions of this file. My recommendation would be to use the codeCachePath option and store it within there since that's always assumed to be a local path, and should always be shared across the instances since it contains the compiled templates
 
Last edited:
Another potential bug I've come across - it doesn't seem to use the fs adapters for the mmdb file, I store internal data in an S3 bucket on several sites, but it's hardcoding it into an internal_data/zee directory, which in some cases is okay, but in the cases where we have multiple webservers it's leading to inconsistencies because different webservers are ending up with different versions of this file. My recommendation would be to use the codeCachePath option and store it within there since that's always assumed to be a local path, and should always be shared across the instances since it contains the compiled templates

Are you sure about this? I mean, I could make the path configurable - this is probably a better option, this way you can decide to change it from default. I am not sure an S3 mount for a file that will be hit very hard is going to be amazing for performance?

Just one suggestion - could the 'docs' directory be removed from the web route? Seems like something that shouldn't be publicly accessible, and could easily be rendered on an admin page somewhere

I have this request in my backlog. Thank you.
 
Are you sure about this? I mean, I could make the path configurable - this is probably a better option, this way you can decide to change it from default. I am not sure an S3 mount for a file that will be hit very hard is going to be amazing for performance?

Yeah having it configurable separately would work as well, just to be clear I’m not suggesting allowing it to go to S3, the reason I suggested reusing the code cache path is that it’s already defined and already is just a static path, but having a separate config option would work just as well :)
 
I am unable to store a Cloudflare API Token. I've tried it in Firefox and Edge browsers. It simply will not store the API Token.

Help, please.
 
Last edited:
I can only advise everyone to be cautious with this add-on. It has completely wrecked my SEO.

I've reported everything to the developer via DM, and this urgently needs to be investigated.

My Google Search Console Host Errors have exploded.
 
I can only advise everyone to be cautious with this add-on. It has completely wrecked my SEO.

I've reported everything to the developer via DM, and this urgently needs to be investigated.

My Google Search Console Host Errors have exploded.

Hello, "completely wrecked SEO" is perhaps a bit of an over-statement... Have you validated this? Or are you guessing?

I just saw your messages now, you have only mentioned so far that Googlebot reports errors when trying to scrape the /bot-guard/collect URL. I had replied to you earlier with the following, which remains as the answer. I cannot force Googlebot not to crawl a URL, it will try to call any URL it wants to. It gets the 500 error as it is an AJAX only endpoint and so returns the appropriate status code... it sets the appropriate headers to tell bots not to index or crawl the URL. I am not in control of Google itself:

It’s normal for Googlebot to request /bot-guard/collect. That endpoint is part of Bot Guard’s browser validation flow, so it remains reachable, but it is not indexable forum content.

XF Bot Guard already marks Bot Guard public endpoints, including the collector endpoint, with X-Robots-Tag: noindex, nofollow, noarchive, and rendered Bot Guard-owned public pages suppress XenForo’s native Google Analytics include.

There isn’t anything you need to do here. Googlebot visiting that operational URL does not mean it will index it, and it does not mean Googlebot will be blocked from crawling the rest of the site.

Legitimate Google crawler traffic is explicitly allowed through Bot Guard on normal site pages when Allow verified search crawlers and fetchers is enabled, after Bot Guard verifies the request against the supported crawler data rather than trusting the user-agent string alone.

So the recommended approach is to leave the collector endpoint alone and keep the verified crawler allowance enabled. Googlebot hits to /bot-guard/collect in logs are expected and harmless.
 
Back
Top Bottom