Every 403 we hit last week came from our own infrastructure. Not an attacker, not an outage — our own web application firewall, refusing our own requests, on our own site. That part is ordinary; anyone running a managed host with a WAF in front of WordPress will meet it. What is worth writing down is that we diagnosed two of the three wrong before we diagnosed them right, and both times the mistake had the same shape.
We run a small English-language site on a Japanese managed host. The publishing pipeline is automated: a checker runs eight gates against a draft and posts it through the REST API if all eight pass. So when the firewall says no, it says no to a script, with no browser and no human watching. You get a 403 and an HTML page that tells you nothing.

The first one: a login that worked everywhere else
A login POST returned 403 while a GET of the same page returned 200. We concluded the site had a block specific to itself — it is the newer of our two properties, and it had an overseas-access restriction switched on that the older site did not. We turned that restriction off.
It made no difference, because it was never the cause. The real cause was a separate setting in the host’s control panel, a login restriction on the admin screen, sitting one menu below the thing we had changed.
What would have caught it in a minute: sending the same request to our other site. We did that eventually and got the same 403 there — from a site that was working perfectly well. The 403 was simply what that endpoint returns to a request with no session cookie. It was never evidence of anything.
We had changed a security setting on a live site based on a difference we never confirmed was a difference.
The second one: three paragraphs the firewall would not accept
An article refused to publish. The gates passed; the POST returned 403. This one we got right, and the reason we got it right is worth naming: we did not reason about it at all. We posted the article one paragraph at a time. Forty-seven went through and three did not.
The three had nothing in common thematically. They had something in common structurally: each contained a comma-separated list inside quotation marks. The firewall rule was a SQL injection filter that treats a comma followed shortly after by the word and as an attack signature — the shape of any ordinary three-item list written with an Oxford comma.
We would print the example here, but the firewall will not carry it. This article is published through the same rule set, and an example is precisely what it refuses. This paragraph took four attempts.
Bisecting is not a clever technique. It is just a comparison baseline applied repeatedly, which is why it works when reasoning does not.
The third one: an upload limit that did not exist
Uploading images to the media library, three succeeded and the fourth failed with 403. Every attempt after that failed too. The obvious reading was a rate limit — three per window, then a cooldown.
So we waited, and retried eight times over ten minutes. All 403.

Then we did the thing we should have done first: took an image that had already uploaded successfully, gave it a new filename, and sent it in the same batch as one that kept failing.
The known-good image returned 200. The failing one returned 403. There was no rate limit and there never had been. The block was a property of the file, not of the sequence — certain PNG byte sequences match a firewall signature, and re-rendering the same figure at a different size produced the same bytes in the same place. Converting it to JPEG uploaded on the first try.
“Three succeeded, then everything failed” felt like strong evidence for a limit. It was a coincidence of ordering, and we spent ten minutes acting on it.
The same mistake twice
Two of the three had the identical structure. We observed a sequence of outcomes, inferred a cause that fit the sequence, and acted on the inference — without once running the case we already knew worked down the same path at the same time.

A comparison baseline costs almost nothing. In the first incident it was one request to our other site. In the third it was one file we had already uploaded. Both were under a minute. Both would have removed the wrong answer immediately, and in the first case they would have saved us from weakening a security setting for no reason.
The reason this bites harder with a WAF than with ordinary bugs is that a firewall gives you a block page instead of an error. There is no stack trace, no rule name, no line number. The absence of information invites you to fill the gap with a theory, and a theory that fits three data points feels like knowledge.
What we actually changed
Not the firewall. That was the other instinct worth resisting.
The same firewall had blocked over two hundred real scans of the site that month. Switching it off to make our own pipeline work would have traded a known ongoing risk for a few minutes of convenience. The host’s WAF log lists every block with the rule that fired and an option to exclude that single rule. We excluded one rule — the SQL injection filter that cannot tell an English list from an attack — and left everything else running.
For the image uploads we changed nothing on the server at all. The pipeline now converts to JPEG and retries when a PNG comes back 403.
If you run the same setup
Three things, in the order they save you time.
Go to the WAF log before you form a theory. It names the rule. Everything we worked out by bisecting was sitting in that log the whole time, with the matched string highlighted.
Send the known-good case down the same path, at the same moment. Not yesterday’s result, not the other environment’s result — the same path, now. If it also fails, your theory about what makes the failing case special is already wrong.
Exclude rules, don’t disable the firewall. A single-rule exclusion is auditable and reversible. “We turned it off during the incident” is neither, and it tends to stay off.
None of this is sophisticated. It is the discipline of confirming a difference before building on it, which is exactly the discipline that is hardest to hold when you are three hours in and the deploy is blocked.
We have written up the gate system that sits in front of all of this. We have also written up what broke when we handed the work to scheduled agents. The pattern is consistent across all three write-ups: the failures were not caused by the automation being wrong. They were caused by us being confident about something we had not checked.
Editorial note. Written and edited by the operator of this site. Every 403, timing, and retry count described here is from our own logs for the week of 2026-09-08. Where we describe a mistake, we made it.
Disclosure. This article contains no affiliate or sponsored links. This site may earn commissions from links in other articles, and those articles carry their own disclosure at the top. Nothing here is professional advice.
