Your Page Builder Is Getting You Banned From Your Own API

Implementation

LittleBig.Co

What if the tool you trust to build your site is also the one triggering your security stack to lock you out?

Full transparency: Links marked with (*) are affiliate links. Yes, we might earn a commission if you buy. No, that doesn’t mean we’re shilling garbage. We recommend what we’d actually use ourselves. Read our full policy.

The Assumption Everyone Makes

WordPress page builders are design tools. They talk to the REST API, sure, but that’s an implementation detail nobody needs to worry about. The API is there, the builder uses it, pages get built. Meanwhile, security tools like CrowdSec or Fail2Ban watch for abusive API traffic patterns and block offenders. These are two separate concerns that don’t interfere with each other.

What Actually Happens

Etch is a premium WordPress page builder aimed at professional developers – not a lightweight drag-and-drop toy. It doesn’t constantly poll the API. But when it needs something – loading a template, opening a component library – it requests everything it might need in one burst. More than 40 API calls per second. For the builder, that’s efficient: grab all resources at once rather than lazy-loading them piecemeal. For your security stack, that’s indistinguishable from an automated attack.

CrowdSec sees 40+ rapid-fire requests to /wp-json/ from a single IP, matches it against a scenario like crowdsecurity/http-probing, and bans you. The cascade is immediate and disorienting. You load a template, the API calls fire, and within seconds CrowdSec’s bouncer starts returning 403s. The builder partially renders – some CSS and assets are already blocked, so the layout looks broken. You assume something glitched and reload. That triggers another burst of API calls from an already-banned IP. Now you’re staring at CrowdSec’s block screen instead of your page builder, locked out of your own site by your own security tools.

Why This Matters More Than You Think

WordPress doesn’t log REST API activity by default. Your access logs show hits to /wp-json/, but they won’t tell you the authenticated user, the parsed route, or the request frequency per session. Without that visibility, you can’t distinguish between a legitimate page builder session and an actual attack. You’re left guessing which layer returned the 403 – Cloudflare, CrowdSec, NGINX, or WordPress itself.

This isn’t just an Etch builder problem. Any plugin or tool that makes heavy API use – WooCommerce dashboard widgets, headless frontends, external automation via Zapier or Make – can trigger the same pattern. The REST API is the most powerful and most exposed surface on a modern WordPress installation, and almost nobody is watching it.

The Questions You Should Be Asking Instead

How do I log WordPress REST API requests? Drop a must-use plugin into wp-content/mu-plugins/ that hooks into rest_pre_dispatch. This fires before every REST API response, giving you access to the method, route, IP, authenticated user, and user agent. Log it to a file for quick wins or to syslog for production-grade throughput.

How can I tell where a 403 is coming from? If a request appears in your NGINX access log with a 403 but does not appear in your API log, the block happened before WordPress — meaning CrowdSec, your reverse proxy, or a server-level rule intercepted it. If it does appear in both logs, WordPress itself returned the 403.

How do I stop my security stack from blocking legitimate API traffic? First, you need visibility. Log the API traffic, identify the patterns your page builder creates, then whitelist those patterns or tune your CrowdSec scenarios to exclude authenticated admin sessions from rate limiting.

What This Looks Like in Practice

Quick Start: File-Based Logging

For most sites, writing to a dedicated log file is the fastest path to visibility. Drop this into wp-content/mu-plugins/api-request-logger.php:

<?php
/**
 * Plugin Name: REST API Request Logger
 * Description: Logs all REST API requests to a dedicated file.
 */

add_filter('rest_pre_dispatch', function ($result, $server, $request) {
    $log_file = WP_CONTENT_DIR . '/api-requests.log';

    $entry = sprintf(
        "[%s] %s %s | IP: %s | User: %s | Agent: %s\n",
        current_time('Y-m-d H:i:s'),
        $request->get_method(),
        $request->get_route(),
        $_SERVER['REMOTE_ADDR'] ?? 'unknown',
        get_current_user_id() ?: 'anonymous',
        $_SERVER['HTTP_USER_AGENT'] ?? 'none'
    );

    file_put_contents($log_file, $entry, FILE_APPEND | LOCK_EX);

    return $result;
}, 10, 3);

This gives you a log at wp-content/api-requests.log with the method, route, IP, authenticated user, and user agent for every API request. Block direct access to it via .htaccess:

<Files "api-requests.log">
    Require all denied
</Files>

This approach works fine for moderate traffic. If your site handles thousands of API requests per minute, file locking becomes a bottleneck. That’s when you move to syslog.

Production-Grade: Syslog-Based Logging

Replace the file-based logger with this version that hands off all write concurrency to syslog:

<?php
/**
 * Plugin Name: REST API Request Logger (Syslog)
 * Description: High-throughput REST API logging via syslog.
 */

add_filter('rest_pre_dispatch', function ($result, $server, $request) {
    openlog('wordpress-api', LOG_PID | LOG_NDELAY, LOG_LOCAL0);

    syslog(LOG_INFO, sprintf(
        '%s %s|%s|uid:%s|%s',
        $request->get_method(),
        $request->get_route(),
        $_SERVER['REMOTE_ADDR'] ?? 'unknown',
        get_current_user_id() ?: 'anonymous',
        $_SERVER['HTTP_USER_AGENT'] ?? 'none'
    ));

    return $result;
}, 10, 3);

No file locking, no buffering concerns, no permissions issues. Syslog handles the concurrency, buffering, and rotation natively. The LOG_LOCAL0 facility gives you a dedicated channel that won’t mix with other application logs.

Where Most People Get Stuck

The common mistake is treating this as a binary problem — either everything works or the API is “broken.” The reality is layered. A request passes through Cloudflare, then your web server, then CrowdSec’s bouncer, then WordPress itself. A 403 can originate at any of those layers, and each one has its own logic and its own logs.

The second mistake is assuming Cloudflare is the culprit. If you see 403s in your NGINX access log, Cloudflare already let the request through — it’s something downstream. That single observation eliminates an entire layer from your investigation.

The third mistake is not logging the API at all and then wondering why a page builder session that fires 200 API calls in five minutes gets treated like an attack.


Strategic Opposition Principle: Your security tools can only distinguish legitimate traffic from attacks if you give them the context to tell the difference. Without API-level logging, your own workflow is indistinguishable from a threat.


Appendix A: rsyslog Configuration

Route the local0 facility to a dedicated file. Create /etc/rsyslog.d/wordpress-api.conf:

local0.* /var/log/wordpress/api-requests.log

The selector local0.* matches all severity levels on the local0 facility — the same facility specified in the PHP openlog() call. Prepare the directory and restart:

mkdir -p /var/log/wordpress
chown syslog:adm /var/log/wordpress
systemctl restart rsyslog

Using a drop-in file in rsyslog.d/ keeps configuration modular — especially relevant when managing multiple WordPress installations on the same server.

Appendix B: CrowdSec — Identifying False Positives

If your 403s trace back to CrowdSec’s NGINX bouncer, identify the triggering scenario:

cscli decisions list -o json | jq '.[] | {ip: .value, scenario: .scenario, alert_id: .alert_id}'

Inspect the alert to see the actual events that triggered the ban:

cscli alerts inspect <ALERT_ID>

This shows the parsed log lines — the exact requests, paths, and frequency that caused the ban. Common false positive triggers for legitimate API traffic include crowdsecurity/http-probing and crowdsecurity/http-bad-user-agent.

To whitelist a known-good IP:

cscli decisions delete --ip 1.2.3.4
cscli whitelist add --ip 1.2.3.4 --reason "Legitimate API client"

Appendix C: Wazuh Integration

Feed CrowdSec activity into Wazuh by monitoring the CrowdSec log. In your Wazuh agent config (/var/ossec/etc/ossec.conf):

<localfile>
  <log_format>syslog</log_format>
  <location>/var/log/crowdsec.log</location>
</localfile>

Custom decoder (/var/ossec/etc/decoders/crowdsec_decoder.xml):

<decoder name="crowdsec">
  <prematch>crowdsec</prematch>
</decoder>

<decoder name="crowdsec-decision">
  <parent>crowdsec</parent>
  <regex>ip=(\S+)\s+scenario=(\S+)\s+action=(\S+)</regex>
  <order>srcip, data, action</order>
</decoder>

Custom rules (/var/ossec/etc/rules/crowdsec_rules.xml):

<group name="crowdsec,">
  <rule id="100200" level="5">
    <decoded_as>crowdsec</decoded_as>
    <description>CrowdSec event detected</description>
  </rule>

  <rule id="100201" level="10">
    <if_sid>100200</if_sid>
    <match>action=ban</match>
    <description>CrowdSec banned IP: $(srcip) - Scenario: $(data)</description>
  </rule>
</group>

Adjust the decoder regex to match your CrowdSec log format — check tail -50 /var/log/crowdsec.log first. With this in place, CrowdSec bans, WordPress API activity, and NGINX access logs all correlate in one dashboard.