September 30, 2026
The Tiny Project Logic Bug That Unlocked Enterprise
How a tiny edge case in project grouping turned into a subscription-boundary bypass.

By Krishn Babariya
6 min read
There are some bugs you go looking for.
And then there are bugs you stumble into because you ask a slightly ridiculous question.
This was the second kind.
I was looking at a project-management feature that allowed Enterprise projects to be grouped with child projects. My test account, however, was on the Free plan.
The normal workflow wasn't available to me.
So I started wondering: What exactly is the backend checking?
That question led to one of the strangest privilege escalations I've found.
A project that started as:
plan: free
enterprise: falseplan: free
enterprise: falseeventually became:
plan: enterprise
enterprise: trueplan: enterprise
enterprise: trueAnd the trigger was surprisingly small:
[{ "_id": "<the project's own ID>" }][{ "_id": "<the project's own ID>" }]The starting point: a Free project
I had a normal Free-tier project.
Nothing special.
I queried the project's state directly so I knew exactly what I was starting with:
At this point, there was no Enterprise entitlement involved.
The first thing I tried didn't work
The project-grouping feature itself was supposed to be an Enterprise-only capability.
From a Free account, the normal child-project workflow was not available. That actually made me more interested.
Instead of assuming: "Free users can't use project grouping."
I wanted to know: "Does the server actually enforce that rule?"
So I looked at the underlying project relationship endpoint:
PATCH /api/projects/<project>/childrenPATCH /api/projects/<project>/childrenThe endpoint accepts project IDs that are supposed to become children of the parent.
I first thought about the obvious test: add another project as a child.
That did not give me anything useful from the Free account.
So I changed the question.
The weird test
Every parent/child system has an assumption hiding inside it:
A project cannot logically be its own child.
It's such an obvious assumption that developers may never explicitly validate it.
So I took the project ID I already had and sent it back as its own child.
The request was conceptually:
PATCH /api/projects/<project>/children
Content-Type: application/json
X-XSRF-TOKEN: <redacted>PATCH /api/projects/<project>/children
Content-Type: application/json
X-XSRF-TOKEN: <redacted>with:
[
{ "_id": "<same-project-id>" }
][
{ "_id": "<same-project-id>" }
]I expected:
400 Bad Request400 Bad Requestor at least some validation error.
Instead, the server accepted it.
The response included the project's own ID in:
childrenProjectschildrenProjectsThat meant the relationship was not only accepted by the endpoint โ it was being persisted.
And then something even stranger happened.
At first, it looked like nothing happened
The immediate mutation response still said:
plan: free
enterprise: falseplan: free
enterprise: falseSo for a moment, I thought the self-reference was just a weird database edge case with no security impact.
But I had learned to distrust mutation responses when the application has derived state.
So I refreshed.
And that changed everything.
where the same project now reports:
plan: enterprise
enterprise: trueplan: enterprise
enterprise: trueThe dashboard also exposed an Enterprise Dashboard entry.
That was the moment I stopped thinking about "bad project validation" and started thinking about privilege escalation.
The UI wasn't the interesting part
I made the Enterprise badge appear. A frontend could display the wrong thing. So I checked the backend again.
After a fresh fetch:
plan = enterprise
enterprise = trueplan = enterprise
enterprise = trueThe server itself was now treating the project as Enterprise.
The bug wasn't just cosmetic.
Then the self-reference broke the project
There was a catch.
Because I had created a circular project relationship, the public documentation route started following that relationship over and over.
Conceptually:
parent
โ
parent
โ
parent
โ
parent
โ
...parent
โ
parent
โ
parent
โ
parent
โ
...The browser eventually hit:
ERR_TOO_MANY_REDIRECTSERR_TOO_MANY_REDIRECTSAt first I thought:
Great. I found the escalation, but I also broke the project. That wasn't exactly what I wanted. So I asked another question:
Can the Enterprise state be used without relying on the broken self-referencing project?
That led to the cleaner part of the exploit.
A clean child project
I used a separate project as the child.
Then I added that clean child to the now-Enterprise parent through the same grouping functionality.
I refreshed the child.
And the child came back as:
This was the part I found most interesting.The malformed self-reference did not have to be my end state.
It could act as the Enterprise anchor.
A separate, clean child could then inherit the Enterprise state.That made the issue much more than a funny self-reference.
What had actually been bypassed?
The real problem was not simply:
parent == childparent == childThe deeper problem was:
invalid project relationship
โ
trusted by entitlement logic
โ
Enterprise stateinvalid project relationship
โ
trusted by entitlement logic
โ
Enterprise stateIn other words, user-controlled project relationships were influencing a security-sensitive authorization decision.
That is a dangerous pattern.The application needed to answer:
"Does this project actually have Enterprise entitlement?"
Instead, the effective state could be changed by manipulating the project graph.
The boundary looked like this:
Free authenticated user
โ
project grouping endpoint
โ
self-reference accepted
โ
Enterprise state becomes effective
โ
Enterprise dashboard
โ
clean child can inherit Enterprise stateFree authenticated user
โ
project grouping endpoint
โ
self-reference accepted
โ
Enterprise state becomes effective
โ
Enterprise dashboard
โ
clean child can inherit Enterprise stateThat is why I classified this as a privilege escalation / business-logic vulnerability, not just a validation bug.
Why I submitted it as Critical
I originally submitted the finding as Critical.
The reason was the boundary being crossed.
I was not simply changing something inside a Free project.
I was starting with: Free and obtaining = ENTERPRISE
without the normal Enterprise purchase/provisioning path.
From my perspective, that is a particularly serious class of business-impact bug because the application is effectively giving a lower-tier account a higher-tier commercial entitlement.
I considered that more important than the raw complexity of the payload.
The payload itself was tiny.
The boundary it crossed was not.
The program's final assessment
The program ultimately accepted the report as:
HIGH SEVERITY
$1,000 BOUNTYHIGH SEVERITY
$1,000 BOUNTY
I still think there is an interesting distinction here.
A CVSS score measures technical security characteristics.
It does not perfectly capture:
"Can a free user obtain an expensive commercial entitlement?""Can a free user obtain an expensive commercial entitlement?"That is why I originally argued for Critical based on the business impact, while the program's final classification was High.
And honestly, the final result made the finding even more interesting to me.
The hard part wasn't writing the payload.
The hard part was finding the trust relationship that the payload could abuse.
How I found it
Looking back, the investigation was basically a chain of increasingly weird questions:
Project grouping is Enterprise-only
โ
What is the backend endpoint?
โ
Can a Free account reach it?
โ
Normal child association doesn't work
โ
What if the parent is its own child?
โ
Self-reference accepted
โ
Refresh
โ
Why is the plan now Enterprise?
โ
Can a clean child inherit it?
โ
Yes.Project grouping is Enterprise-only
โ
What is the backend endpoint?
โ
Can a Free account reach it?
โ
Normal child association doesn't work
โ
What if the parent is its own child?
โ
Self-reference accepted
โ
Refresh
โ
Why is the plan now Enterprise?
โ
Can a clean child inherit it?
โ
Yes.That is probably the biggest lesson I took from the bug.
The interesting vulnerabilities often sit one layer below the feature people are actually using.
The root cause
There appear to be two separate trust failures.
1. The project graph accepts an invalid relationship
The backend should reject:
child_project_id == parent_project_idchild_project_id == parent_project_idand should also prevent circular project relationships more generally.
2. Enterprise entitlement is influenced by that graph
Even more importantly, Enterprise state should come from an authoritative subscription or organization entitlement.
It should not effectively become:
childrenProjects
โ
EnterprisechildrenProjects
โ
EnterpriseA safer design is:
Billing / organization entitlement
โ
server-side authorization
โ
Enterprise featuresBilling / organization entitlement
โ
server-side authorization
โ
Enterprise featuresThe project graph can describe relationships.
It should not decide who has paid for Enterprise.
The lesson
This bug changed how I think about "relationship" endpoints.
An endpoint like:
/children/childrenlooks harmless.
But relationship data can become security-sensitive when another subsystem trusts it.
That means these are worth testing carefully:
parent / child
owner / member
group / project
account / organization
resource / entitlementparent / child
owner / member
group / project
account / organization
resource / entitlementA surprisingly small data-model mistake can become a privilege escalation when those relationships influence authorization.
The question I now try to ask is not only:
"Can I modify this object?"
but also:
"Can I modify a relationship that changes what the application believes this object is entitled to?"
Final thoughts
The exploit was only a few lines.
The investigation was the interesting part.
I started with a Free project and an Enterprise-only feature I couldn't normally use.
Then I tried something that looked almost too silly to matter:
[{ "_id": "<own-project-id>" }][{ "_id": "<own-project-id>" }]That invalid relationship was accepted.
After a refresh, a Free project had become Enterprise.
The program accepted the finding as High severity and awarded a $1,000 bounty.
How easily a small assumption in a project relationship could reach all the way up into the application's subscription boundary.
Sometimes the weirdest test is the one worth sending.