0xmrma/CVE-2026-46552
GitHub: 0xmrma/CVE-2026-46552
Stars: 0 | Forks: 0
# CVE-2026-46552
NocoDB Shared-Base Links Could Invite Real Base Members and Survive Share Revocation
## Intro
I found this issue while reviewing **NocoDB**, with a simple security question in mind:
**Can a public shared-base link cross the boundary from temporary shared access into real authenticated base membership?**
In this case, the answer was yes.
A shared-base session authenticated only by `xc-shared-base-id` was treated as a normal base viewer for ACL purposes. Because viewer permissions still reached member-management endpoints, a user with only the shared-base UUID could enumerate existing base members and invite an arbitrary email address into the base as a real member.
That invited user could then redeem the invite through the normal signup flow, obtain a standard authenticated account, and keep base access even after the owner disabled the shared-base link.
That issue became **CVE-2026-46552**.
**Project:** [NocoDB](https://github.com/nocodb/nocodb)
**Affected version validated:** `0.301.3`
## Attack Chain
`public shared-base link -> xc-shared-base-id treated as normal base viewer -> viewer ACL reaches member-management endpoints -> attacker lists base users and invites arbitrary email -> invited user redeems normal signup token -> durable authenticated base access survives shared-link revocation`
## What NocoDB Does
**NocoDB** is a database-oriented collaboration platform that exposes browser-based base access, sharing, metadata management, and user membership workflows.
That means its sharing model is a real security boundary.
The important question here was not whether shared-base links can read shared content.
The real question was:
**Can a public share principal perform actions that should belong only to authenticated base members?**
In this case, it could.
## Why This Surface Was Worth Looking At
Public sharing features are easy to underestimate.
That is a mistake.
Once an application supports:
- anonymous or link-based access,
- role mapping,
- and ordinary authenticated management APIs behind the same ACL system,
the main risk is not just data exposure.
The stronger risk is **boundary collapse**:
- a lower-trust principal inherits higher-trust capabilities,
- management actions become reachable from a public-share context,
- and temporary access can be converted into persistent access.
That was the real issue here.
This was not a bug in login validation.
It was not a token forgery issue.
It was not a password-reset flaw.
It was a classic **authorization boundary failure**:
- a shared-link principal was mapped into normal base roles,
- those roles still included member-management capabilities,
- and real durable access-control state could then be modified from the public-share context.
## The Boundary I Focused On
I did not approach this by randomly probing endpoints and hoping something interesting would respond.
The stronger path was to identify the highest-value trust boundary first.
For NocoDB, that was the boundary between:
- **shared-base access**
- and **authenticated base membership**
Those two states should not be interchangeable.
A shared-base link is supposed to represent scoped, revocable, link-based access.
It should not be able to mint new long-lived principals inside the base.
That is the exact boundary that failed here.
## Root Cause
The vulnerability came from how shared-base access was integrated into the normal ACL path.
In the shared-base frontend flow, `xc-shared-base-id` was injected while ordinary auth headers were removed.
Then, in the backend, `BaseViewStrategy` accepted `xc-shared-base-id` and translated the shared link directly into ordinary `roles` / `base_roles` derived from the shared-base configuration.
That was the first problem.
The second problem was that **viewer-level permissions still included member-management actions**.
In the ACL layer, `ProjectRoles.VIEWER` could reach:
- `baseUserList`
- `userInvite`
Those permissions guarded normal meta routes:
- `GET /api/v2/meta/bases/:baseId/users`
- `POST /api/v2/meta/bases/:baseId/users`
So a public-share session was effectively allowed to hit membership endpoints intended for real base participants.
The last step was in the invite flow itself.
`BaseUsersService.userInvite()` checked role power, then created:
- a real user row with an `invite_token`
- a real base-membership row for the target base
And for shared-base sessions:
- `invited_by` became `null`
because there was no real authenticated inviter identity behind the request.
That is the whole bug chain.
### Why this is exploitable
Because possession of a shared-base link was enough.
The attacker did not need:
- `xc-auth`
- a pre-existing account
- stolen credentials
- or prior membership in the base
The exploit chain was straightforward:
- attacker obtains a shared-base UUID
- the UUID is accepted as a base viewer principal
- viewer ACL reaches member-management endpoints
- attacker lists current base members
- attacker invites an arbitrary email address
- the invited user redeems the token through the normal signup flow
- the new account becomes a real authenticated base member
- the owner later disables the shared link
- the invited account still keeps normal authenticated access
That converts revocable link-sharing into durable membership.
## What Makes This a Security Issue, Not Just Odd Sharing Behavior
The important distinction is persistence across revocation.
This was not just:
The vulnerable principal was not a normal authenticated viewer.
It was a **public-share session**.
That matters because the application treated a transient, link-scoped principal as if it were trusted enough to:
- enumerate real members,
- change access-control state,
- and create new durable principals inside the base
The real question was not:
The real question was:
The answer was yes.
That is why this is a real authorization vulnerability rather than just surprising application behavior.
## PoC
I validated this locally against:
- product version: `0.301.3`
- commit: `dac49b0122c5ee655fb8f46a1b6e42dfeec1f3ad`
- base URL: `http://127.0.0.1:8080`
The reproduction was straightforward.
First, I signed in as a normal owner account, created a fresh base, created a table, and enabled shared-base access as `viewer`:
PATCH /api/v2/meta/bases//shared
Content-Type: application/json
{
"roles": "viewer"
}
That returned the shared-base UUID.
Then, without sending any `xc-auth`, I used only:
xc-shared-base-id:
Using only that header, I called:
GET /api/v2/meta/bases//users
That returned `200 OK` and exposed real base members, including email addresses.
Still using only `xc-shared-base-id`, I called:
POST /api/v2/meta/bases//users
Content-Type: application/json
{
"email": "attacker@example.com",
"roles": "viewer"
}
That also returned `200 OK`.
For local lab validation without email delivery, I confirmed directly in the SQLite meta database that:
- `nc_users_v2` contained the invited user with a non-null `invite_token`
- `nc_base_users_v2` contained a real membership row for the target base
- `invited_by` was `NULL`
Then I redeemed the invite through the normal signup flow:
POST /api/v2/auth/user/signup
Content-Type: application/json
{
"email": "attacker@example.com",
"password": "Password123.",
"token": ""
}
Using the returned `xc-auth`, I called:
GET /api/v2/meta/bases//tables
That returned `200 OK`.
Finally, as the owner, I disabled the shared-base link:
DELETE /api/v2/meta/bases//shared
After that:
- shared-link access with `xc-shared-base-id` failed with `401`
- the invited account using normal `xc-auth` still succeeded with `200`
### Observed results
- shared user list: `200`
- shared invite: `200`
- signup: `200`
- invited authenticated table access before share disable: `200`
- share disable: `200`
- shared-link table access after disable: `401`
- invited authenticated table access after disable: `200`
That established the central security claim:
- public share access could reach membership endpoints
- membership changes created real durable authenticated access
- revoking the original share did not remove that access
## Why the Reproduction Matters
One successful shared-base invite would already have been enough to show an authorization failure.
But the full validation chain mattered for two reasons.
### First
It showed that this was not just endpoint exposure.
The public-share session did not merely reach a restricted API.
It completed the full privilege-conversion chain:
- enumerate members
- invite a new principal
- redeem the invite
- obtain normal authenticated access
### Second
It proved this was not self-revoking.
The more serious impact came after shared-link disablement:
- the original link died
- the attacker-created account did not
That is what turned temporary link access into durable access persistence.
## Impact
This vulnerability allows anyone with a shared-base link to:
- enumerate real base members and their email addresses
- invite arbitrary email addresses into the base as real members
- convert temporary link-based access into persistent authenticated membership
- preserve that access even after the owner revokes the shared link
The primary impact is **confidentiality**, because an attacker can maintain lasting read access to shared-base data through an ordinary authenticated account.
There is also **integrity impact**, because a public-share principal can modify access-control state by adding new members to the base.
That is a stronger outcome than ordinary data leakage.
It is a privilege-boundary break between anonymous sharing and authenticated membership.
## Severity and Classification
This issue is reasonably classified as a cross-scope authorization flaw with confidentiality impact.
**CVSS:**
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N
That vector fits the core behavior here:
- network-reachable behavior
- no prior authenticated account required
- no user interaction required from the victim during exploitation
- scope changes because a public-share principal crosses into normal membership-management capability
- confidentiality impact through lasting unauthorized base access
## Suggested Mitigation
The fix direction is straightforward.
Shared-base sessions should not inherit member-management capabilities.
At minimum:
- remove `baseUserList` and `userInvite` from any permissions reachable through `xc-shared-base-id`
- enforce an explicit block so shared/public principals cannot call base membership endpoints such as `GET` and `POST /api/v2/meta/bases/:baseId/users`
- treat shared-base access as a distinct principal type instead of mapping it directly onto normal base viewer permissions
- add regression tests that verify shared-base requests cannot enumerate members, cannot invite users, and cannot create durable access that survives share revocation
## Disclosure
This issue was validated locally against NocoDB `0.301.3` on commit `dac49b0122c5ee655fb8f46a1b6e42dfeec1f3ad`.
The report demonstrated:
- the ACL mapping from `xc-shared-base-id` into normal base roles
- the viewer permission path into member-management endpoints
- the creation of real invited users and base-membership rows
- the ability to redeem the invitation through the normal signup flow
- the persistence of authenticated access after shared-link revocation
The issue was assigned:
**CVE-2026-46552**
## What This Bug Actually Teaches
The key lesson here is simple:
A lot of systems get into trouble when they collapse those two ideas into the same role model.
A shared link may look operationally similar to a viewer account, but the trust assumptions are different:
- shared links are easy to redistribute
- shared links are meant to be revocable
- shared links are usually lower-assurance principals
If that lower-assurance principal can perform management actions or mint new durable identities, the sharing boundary is already broken.
That is the real takeaway.
## Key Points
- public sharing features are security boundaries
- shared-link principals should not inherit ordinary membership-management capabilities
- member enumeration from a public-share context is already sensitive
- unauthorized invitation is worse because it creates real durable principals
- revoking the original share is not enough if the attacker-created account survives
- treating public-share access as a separate principal type is the safer design
## Final Words
This vulnerability was not about bypassing authentication entirely.
It was about collapsing two trust levels that should have remained separate.
In NocoDB, a shared-base link was supposed to provide temporary, revocable access to shared content.
Instead, it could be used to enumerate members, invite a real user into the base, and convert public-share access into durable authenticated membership that survived share revocation.
That is why this became **CVE-2026-46552**.

## Attack Chain
`public shared-base link -> xc-shared-base-id treated as normal base viewer -> viewer ACL reaches member-management endpoints -> attacker lists base users and invites arbitrary email -> invited user redeems normal signup token -> durable authenticated base access survives shared-link revocation`
## What NocoDB Does
**NocoDB** is a database-oriented collaboration platform that exposes browser-based base access, sharing, metadata management, and user membership workflows.
That means its sharing model is a real security boundary.
The important question here was not whether shared-base links can read shared content.
The real question was:
**Can a public share principal perform actions that should belong only to authenticated base members?**
In this case, it could.
## Why This Surface Was Worth Looking At
Public sharing features are easy to underestimate.
That is a mistake.
Once an application supports:
- anonymous or link-based access,
- role mapping,
- and ordinary authenticated management APIs behind the same ACL system,
the main risk is not just data exposure.
The stronger risk is **boundary collapse**:
- a lower-trust principal inherits higher-trust capabilities,
- management actions become reachable from a public-share context,
- and temporary access can be converted into persistent access.
That was the real issue here.
This was not a bug in login validation.
It was not a token forgery issue.
It was not a password-reset flaw.
It was a classic **authorization boundary failure**:
- a shared-link principal was mapped into normal base roles,
- those roles still included member-management capabilities,
- and real durable access-control state could then be modified from the public-share context.
## The Boundary I Focused On
I did not approach this by randomly probing endpoints and hoping something interesting would respond.
The stronger path was to identify the highest-value trust boundary first.
For NocoDB, that was the boundary between:
- **shared-base access**
- and **authenticated base membership**
Those two states should not be interchangeable.
A shared-base link is supposed to represent scoped, revocable, link-based access.
It should not be able to mint new long-lived principals inside the base.
That is the exact boundary that failed here.
## Root Cause
The vulnerability came from how shared-base access was integrated into the normal ACL path.
In the shared-base frontend flow, `xc-shared-base-id` was injected while ordinary auth headers were removed.
Then, in the backend, `BaseViewStrategy` accepted `xc-shared-base-id` and translated the shared link directly into ordinary `roles` / `base_roles` derived from the shared-base configuration.
That was the first problem.
The second problem was that **viewer-level permissions still included member-management actions**.
In the ACL layer, `ProjectRoles.VIEWER` could reach:
- `baseUserList`
- `userInvite`
Those permissions guarded normal meta routes:
- `GET /api/v2/meta/bases/:baseId/users`
- `POST /api/v2/meta/bases/:baseId/users`
So a public-share session was effectively allowed to hit membership endpoints intended for real base participants.
The last step was in the invite flow itself.
`BaseUsersService.userInvite()` checked role power, then created:
- a real user row with an `invite_token`
- a real base-membership row for the target base
And for shared-base sessions:
- `invited_by` became `null`
because there was no real authenticated inviter identity behind the request.
That is the whole bug chain.
### Why this is exploitable
Because possession of a shared-base link was enough.
The attacker did not need:
- `xc-auth`
- a pre-existing account
- stolen credentials
- or prior membership in the base
The exploit chain was straightforward:
- attacker obtains a shared-base UUID
- the UUID is accepted as a base viewer principal
- viewer ACL reaches member-management endpoints
- attacker lists current base members
- attacker invites an arbitrary email address
- the invited user redeems the token through the normal signup flow
- the new account becomes a real authenticated base member
- the owner later disables the shared link
- the invited account still keeps normal authenticated access
That converts revocable link-sharing into durable membership.
## What Makes This a Security Issue, Not Just Odd Sharing Behavior
The important distinction is persistence across revocation.
This was not just:
The vulnerable principal was not a normal authenticated viewer.
It was a **public-share session**.
That matters because the application treated a transient, link-scoped principal as if it were trusted enough to:
- enumerate real members,
- change access-control state,
- and create new durable principals inside the base
The real question was not:
The real question was:
The answer was yes.
That is why this is a real authorization vulnerability rather than just surprising application behavior.
## PoC
I validated this locally against:
- product version: `0.301.3`
- commit: `dac49b0122c5ee655fb8f46a1b6e42dfeec1f3ad`
- base URL: `http://127.0.0.1:8080`
The reproduction was straightforward.
First, I signed in as a normal owner account, created a fresh base, created a table, and enabled shared-base access as `viewer`:
PATCH /api/v2/meta/bases/