Blog

Why every button on our admin panel said Forbidden

On 13 October 2026 we deployed a new page on our private admin panel, the tool we use to publish our own videos and run our email follow-ups. Its first button was a harmless one: test the connection to our mailbox. We pressed it, and the panel answered with one word: Forbidden.

Nothing had reached the mailbox. Our own security check had refused the click before it got that far. And as we found within the hour, it would have refused every form button on the panel.

The check that said no

The panel sits behind a login, but a login alone does not stop cross-site request forgery: another website quietly submitting a form to the panel through a browser that is already signed in. So every request that changes something must prove it came from the panel’s own pages. The check is simple. Browsers attach an Origin header to a form submission, naming the site the form was on. If it is not the panel’s own address, the request is refused and logged.

Our log said exactly that: request.cross-site-refused, for the button we had just pressed. The browser had told the panel that the form came from somewhere else.

What the browser actually sent

It had not sent a wrong address. It had sent Origin: null.

The cause was another header we had added on purpose. For privacy, every page on the panel sent Referrer-Policy: no-referrer, so that following a link out of the panel would tell the next site nothing about where you came from. That part worked.

The Fetch standard, the specification browsers follow for requests, has a rule we had missed. When a request is not a script request in CORS mode and its method is not GET or HEAD, which describes every ordinary form submission, the browser works out the Origin header from the page’s referrer policy. Under no-referrer, the origin is written as null. A privacy setting for links had quietly removed the one piece of information our security check relied on.

One page on the panel had kept working, and that was the clue. Our video upload page starts its upload with fetch(), a script request in CORS mode, and for those the browser always sends the real origin. Only the classic forms broke.

Why our tests did not catch it

The panel has more than two hundred automated tests, and several of them press every button. All of them passed, because every test set the Origin header itself, to the correct value. So did the local preview we used to click through the pages in a real browser: it added the right Origin to anything it passed on. Our tests had never once behaved like a browser.

So before fixing anything, we changed the preview to pass the browser’s own Origin through unchanged. Then we pressed a button in Edge. It sent Origin: null and got a 403, exactly what had happened in production. Only then did we change the code.

The fix, and two we turned down

We changed the header to Referrer-Policy: same-origin. The browser now sends the real origin and referrer to the panel’s own pages, and still sends nothing at all to any other site. The privacy goal held, and the security check stayed exactly as strict as it was.

We considered two other fixes and rejected both:

  • Accept Origin: null as well. This would undo the check. A page in a sandboxed iframe, or a document opened from a data: address, also sends null, so an attacker could use either to look exactly like a form on our own page.
  • Check Sec-Fetch-Site instead. Modern browsers send this header, and a page’s scripts cannot forge it, so it is a good second check. But making it the only one would mean trusting every browser to send it, and the origin check already works once the referrer policy is right.

After the change, the same button pressed in Edge sent the panel’s real address and worked, and so did every other form. The panel’s tests now expect same-origin, with a note in the code saying why no-referrer must not come back.

Checking this on your own site

If your site checks Origin on form submissions, which most frameworks’ cross-site request forgery protection does in some form, this is worth five minutes:

  1. Open the page with the form, then your browser’s developer tools, on the Network tab.
  2. Submit the form and select the request. Look for the Origin request header.
  3. If it says null, check the Referrer-Policy header on the page itself. no-referrer causes it. So can a form inside a sandboxed iframe.
  4. Test with a real browser, not only with tools like curl or with test code that writes the headers for you. Those send whatever you tell them to, which is exactly how this hid from us.

What this means for your site

Security headers interact. Each of ours was right on its own: a strict origin check, and a referrer policy that leaks nothing. Together they broke every form, and no test noticed, because the tests never let a real browser decide what to send. When we build or audit a website, the forms get pressed in a real browser with the headers it actually sends, before anyone calls the security finished.

Want a site built this carefully?

Tell us what you’re building. A price and a date come back in the first reply.

Start a project