I'm going to start with the part that most people assume is a shield:
ChatCut raised a $1.35M seed round.
Notably, that is real money. Institutional money. The kind of funding that implies "they have their act together." And yet, while evaluating tools for The Lab, I ran into a security pattern that shows up everywhere — from tiny side-projects to companies with millions of dollars behind them.
Because funding doesn't prevent basic mistakes.
It just makes the blast radius bigger.
The moment I knew the waitlist was living in the UI#
I clicked Try Now on ChatCut, and instead of onboarding I hit a waitlist screen: "enter your invite code."
As such, I did what I always do when something is claimed to be enforced: I opened DevTools and watched the network.
The client hit a validate-invite-code endpoint with a simple JSON payload:
await fetch('https://<redacted>/waitlist/validate-invite-code', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ code: 'A1BC2D' }),
});And the response shape made it clear that one flag was driving the UI:
{
"valid": false,
"error": "This invite code has already been used",
"errorCode": "CODE_USED"
}In essence: the browser asked, "is this good?" and then the browser decided what happened next.
That's the red flag.
Client-side validation is a UX feature. It is not a security boundary.
The waitlist gate in plain terms#
To expand on what this means, the flow communicated something like:
- The server returns a status.
- The front end interprets the status.
- The UI proceeds (or blocks) based on that interpretation.
- And if the server doesn't re-check the condition at the moment it matters, the waitlist is just choreography.
When the browser is trusted to enforce a gate, the "gate" belongs to whoever controls the browser tab.
Waitlist Bypass Method 1: Client-trusted invite validation (response tampering)#
Once I saw that the UI treated valid as authoritative, I used Chrome DevTools "Local Overrides" to intercept the validate-invite-code response and flip it:
{
"valid": true
}Exact Chrome DevTools steps:
- Open DevTools and go to Sources.
- In the left panel, open Overrides and click Select folder for overrides. Pick a local folder and approve access.
- Switch to Network, find the
validate-invite-coderequest, right-click it, and choose Save for overrides. - DevTools opens the saved response under Sources → Overrides. Edit the JSON body (e.g., set
"valid": true) and save. - Re-run the invite flow so the overridden response is served to the page.
That was enough to let the UI proceed to account creation. Because the server didn't re-check eligibility at the moment the account was created, the waitlist became a client-side decision.
What mattered: the UI used the valid flag as the gate. If downstream endpoints didn't independently enforce eligibility, the browser had enough control to decide what happened next.
Security lesson: a waitlist is only real if the server enforces eligibility at the moment it creates the account or grants access. If enforcement only happens in the UI, the "invite required" rule is just a suggestion.
Defensive fix (the only correct fix):
- Enforce eligibility server-side at account creation (and any privileged action).
- Treat invite codes as server-owned facts, not UI state.
- If the response can be forged in the browser, it is not a security control.
Waitlist Bypass Method 2: Alternate sign-up path skipped the eligibility check#
This one shows up constantly: multiple entry points, inconsistent enforcement.
Products ship with:
- email sign-up
- Google OAuth
- magic links
- mobile/desktop variants
If any of those paths skip the waitlist check, the waitlist becomes optional.
In this case, an alternate sign-up route allowed account creation to complete without a definitive server-side "invite eligible" requirement. The email sign-up path hit Supabase's OTP endpoint directly:
await fetch('https://<redacted>.supabase.co/auth/v1/otp', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
email: 'example@demo.com',
data: {},
create_user: true,
gotrue_meta_security: {},
code_challenge: null,
code_challenge_method: null,
}),
});One successful OTP flow was enough to create the account without the invite gate ever being enforced server-side.
What mattered: if account creation can complete without a definitive server-side "invite eligible" requirement, then the waitlist is only true for the happy path.
This is the classic multi-entry-point failure: you guard the front door and forget the side door exists.
Defensive fix:
- Centralize eligibility checks in the auth backend.
- Don't let account creation complete unless the server verifies invite eligibility.
- Ensure every auth path (email, OAuth, mobile, deep links) runs the same enforcement.
What I reported#
I reported two issues:
- Invite validation was effectively client-trusted (UI state could determine access).
- A sign-up path did not consistently enforce waitlist eligibility (account creation could proceed without a hard server-side check).
Why this keeps happening (including at funded companies)#
This is the part people don't like to admit: the failure mode is often rational.
Funded teams optimize for:
- onboarding conversion
- "time to value"
- short iteration cycles
- shipping fast enough to beat competitors
And the easiest way to ship an invite-only UX quickly is to treat the invite as front-end state, not a server-side rule.
Furthermore, the more pressure there is to move fast, the more likely it is that "temporary" shortcuts quietly become production reality.
So yes: companies with millions of dollars in funding are prone to these issues — not because they're incompetent, but because incentives reward speed and optics, and security debt hides until someone looks.
Closing thought#
ChatCut is not special here. This is a category of mistake.
But it's an important one, because it reveals a simple rule:
If the server doesn't enforce it, it's not enforced.
And in 2026, "the browser will keep people honest" is not a strategy.