Bypassing Plan-Based User Seat Limits via Unprotected Invite Endpoint
Moaz Abdelaty

TL;DR A SaaS platform enforced its free-plan user seat limit only in the frontend UI. By sending the invite request directly — bypassing the disabled button — an attacker could add unlimited users to a free-tier organization. The server performed no plan-tier check on the invite endpoint.
Background
While testing a SaaS security platform, I noticed the organization management page showed a disabled "Invite User" button on the free plan, accompanied by an upsell prompt to upgrade for additional seats. The platform's pricing page confirmed the free plan was limited to a single user per organization.
When I see a UI limitation like this I always wonder if it's being enforced in the browser or on the server side. If the answer is "the browser," the "restriction" doesn't exist—the API endpoint is still there and still accepting requests, and the disabled button is all that stands between a free user and the paid feature.
Discovery
I started by reviewing the application's bundled JavaScript. I searched for member related queries using
grep -oE '".{0,}(members|invite|users|invites).{0,}"' app.js
After a lot of analyzing, I found this function
await Gt.post("api/v1/users-invite", {
email: e
})
Then I crafted post request to /api/v1/users-invite
POST /api/v1/users-invite
Content-Type: application/json
{ "email": "target@example.com" }
The code on the client side that was responsible for turning the invite button off checked a local state variable representing the plan tier of the org — if the plan was free and the seat count was at the limit, the invite button was turned off and the invite function did not fire. There was no additional gating in the frontend logic for the API call: it was just a plain old POST call with authentication.
Exploitation
With a valid session on a free-tier organization (one user, at the plan limit), I intercepted the network traffic in Burp Suite and crafted the invite request manually, forwarding it directly to the server without going through the UI:
POST /api/v1/users-invite HTTP/2
Host: [redacted]
Authorization: Bearer <free-plan org token>
Content-Type: application/json
{ "email": "seconduser@cybervuln.com" }
Request & Response — HTTP 200 OK returned on free-plan org at seat limit

Result
The server returned HTTP 200 Created and issued the invite successfully. No plan-tier check was performed on the backend — the seat limit existed entirely as a frontend control. The invited user received the invitation email and was able to join the organization, breaching the plan's stated one-user limit.
The restriction lived only in the UI. Once the disabled button was bypassed, the server accepted the request without question.
Impact
- Any free-tier user could invite an unlimited number of additional members to their organization, fully bypassing the paid plan requirement for additional seats.
- The platform's paid plan upsell — the primary commercial incentive for upgrading — was rendered meaningless. Free users gained access to a key paid feature at no cost.
- No special technical knowledge required beyond the ability to send an HTTP request directly (e.g. via Burp Suite, curl, or any HTTP client).
- The vulnerability required only a valid free-tier account — no elevated privileges, no second account, no preconditions beyond the standard signup flow.
Root Cause
The invite endpoint performed authentication (valid JWT required) but not authorization against the caller's plan tier or current seat count. The seat limit was enforced exclusively client-side by disabling the invite button when the org was at capacity. This is a classic example of trusting the frontend to enforce a business rule that must be enforced on the server.
Remediation
The fix is straightforward: before processing an invite request, the server should:
- Retrieve the requesting org's active plan tier from the billing/subscription record.
- Query the current member count for that org.
- Compare the two — if current members ≥ plan seat limit, return 403 Forbidden with an appropriate error (e.g. seat_limit_reached).
- Only proceed with creating the invite if the check passes.
Client-side restrictions are acceptable as a UX layer on top of server-side enforcement, but they can never substitute for it. Any action gated by a subscription tier must be validated on the server on every request, regardless of what the frontend displays.
Takeaway
Whenever you see a feature that's "locked" behind a plan upgrade, the first thing to check is whether the lock is on the door or just painted on the wall. Find the underlying API call in the JS bundle or by proxying the app on a paid account, then replay it from a free account session. If the server doesn't check, the gate was never real.
Business logic bugs like this one are consistently underreported because they don't look like "hacking" — there's no payload, no injection, no exploit chain. It's just an HTTP request. That's exactly why they slip through: developers test that the button is disabled, not that the endpoint rejects the call.
Moaz Abdelaty
Penetration Tester @CYBERVULN LLC
