Blog

A $10 Domain Beat My Security Check. The Fix Was One Dot.

Before I let real traffic hit my site, I spent a night trying to break my own security. I found a hole, and it is one of the most common origin-validation mistakes in production. The fix was a single dot. Here is the whole thing, receipts included.

The site runs on Netlify functions. One of them guards who is allowed to POST to my backend. I wrote it to reject anything that does not come from my own domain, and in plain English it asked one question: does this request come from vettechhomefront.com? The code looked fine. It was host.endsWith('vettechhomefront.com').

That is a string operation wearing a security badge. endsWith does not know what a domain is. It only checks whether some text ends with some characters. And evilvettechhomefront.com ends with those exact characters. Anyone can register that lookalike for about ten dollars. A request from it would carry a perfectly honest Origin header, and my gate would wave it through.

I did not take my own word for it. I forged the header and ran it. Old code, a request stamped Origin: https://evilvettechhomefront.com, got HTTP 200 and filed a real issue on my own tracker. The lookalike walked in the front door and left a note.

Why this bug is everywhere

This is not an exotic mistake. Origin and CORS checks get written with string matching all over production, at companies far bigger than mine, because endsWith reads like it means “belongs to my domain” when it only means “ends with these letters.” The gap between those two is where the attacker lives.

There was a second hole in the same spirit. The old check also waved through any request with no Origin header at all. Friendly by default is the right instinct for a user experience. It is exactly backwards for a gate. Browsers always send an Origin header on this kind of POST, so refusing the ones that do not costs zero real users and shuts the door on the fakes. Security code fails closed. User-experience code fails open. This bug is a UX instinct applied to the wrong file, which is exactly why it is so common.

The fix is one character. Instead of endsWith('vettechhomefront.com'), check for an exact match or endsWith('.' + domain) with a leading dot. That dot is the difference between a domain boundary and a string coincidence. evilvettechhomefront.com does not end in .vettechhomefront.com. Same test, new code, HTTP 403. Blocked. Commit 2d6b5de.

Distrust the clock before you distrust the fix

One more receipt, because this is the part people skip. Twenty seconds after I pushed the fix, I tested it and got the old behavior back. For a second it looked like the fix had failed. It had not. Deploys take a minute to go live. I waited two minutes, ran the same test, and got the 403. In the field you learn to distrust your read of the situation before you rewrite the plan. Same discipline here. Check the clock before you gut the code.

None of this is exotic, and that is the point. It is one function on a small veteran-owned site, hunted down and fixed in a night. I would rather show you a hole I found in my own work than sell you a wall I never tested. Ground truth before opinion. That is the method, not a slogan.

The copyable version, for anyone guarding a domain: never validate an origin with a bare endsWith(domain). Use an exact hostname match or endsWith('.' + domain) with the leading dot, and treat a missing Origin header as a rejection, not a pass. One dot. Grounded in truth.

Ron Hampton
Founder, VetTech Homefront LLC

← All posts

Put a veteran on the problem

Call or text (513) 278-7137, or email ron@vettechhomefront.com

Contact us