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` photo0 ## 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**. photo0