Démarrer
Un scan a besoin de quatre choses, que la vue d'ensemble de votre espace vous demande dans l'ordre :
- Un domaine vérifié : la preuve que vous contrôlez l'hôte de l'API.
- Une description de l'API : spec OpenAPI, capture HAR ou endpoint GraphQL.
- Deux comptes de test appartenant à deux clients différents (deux tenants), par exemple un utilisateur d'Acme et un utilisateur de Globex sur votre préproduction.
- Un premier scan, lancé depuis la page de l'API. Il dure en général de quelques secondes à quelques minutes.
Visez de préférence une préproduction peuplée de données de test : le scan lit des objets réels de chaque compte pour vérifier qu'ils ne sont pas accessibles depuis l'autre.
Vérifier un domaine
Nous ne scannons que les hôtes dont votre organisation a prouvé le contrôle. Ajoutez l'hôte de l'API (par exemple api.exemple.fr) dans la page Domaines : un jeton de la forme tlc-verify-… s'affiche. Publiez-le de l'une des deux façons suivantes, puis cliquez sur « Vérifier ».
Par un enregistrement DNS
Créez un enregistrement TXT sur le sous-domaine _tenantleak de l'hôte :
_tenantleak.api.exemple.fr. 300 IN TXT "tlc-verify-…"
La propagation prend de quelques secondes à quelques minutes selon votre hébergeur DNS.
Par un fichier
Servez le jeton, seul, dans ce fichier texte :
https://api.exemple.fr/.well-known/tenantleak-verification.txt tlc-verify-…
La preuve est revérifiée automatiquement tous les 30 jours avant un scan. Si elle a disparu, le scan échoue avec un message explicite, sans envoyer de requête à l'API.
Si la connexion des comptes de test passe par un autre de vos domaines (par exemple auth.exemple.fr), vérifiez-le aussi. Les fournisseurs d'identité courants (Auth0, Okta, Cognito, Microsoft Entra, Firebase, Supabase…) n'ont pas besoin d'être vérifiés, à condition d'indiquer l'URL https de leur point de jeton (par exemple https://votre-tenant.eu.auth0.com/oauth/token). De même, si la spec OpenAPI est publiée sur un autre hôte que l'API, cet hôte doit être vérifié ; sinon, importez le fichier.
Décrire l'API
À la création de l'API, indiquez son URL de base (par exemple https://api.exemple.fr), puis une source :
- URL de la spec OpenAPI
- OpenAPI 3 ou Swagger 2, en JSON ou YAML, sur un domaine vérifié (en général celui de l'API). Elle est téléchargée à chaque scan, ce qui suit automatiquement l'évolution de l'API.
- Fichier OpenAPI
- Le même document envoyé une fois, 10 Mo au plus. Pratique quand la spec n'est pas publiée.
- Capture HAR
- Sans spec, ouvrez les outils de développement du navigateur (onglet Réseau), parcourez l'application connecté avec un compte, puis « Enregistrer tout au format HAR ». Les endpoints sont reconstruits à partir des requêtes vers l'hôte de l'API.
- GraphQL (offres Pro et Business)
- Le chemin de l'endpoint (par défaut
/graphql). Le schéma est lu par introspection ; si elle est désactivée, envoyez le résultat d'une introspection (JSON) ou le schéma SDL (.graphql).
Comptes de test
Une identité est un compte de test de votre API. Chaque identité porte un nom (par exemple alice_acme) et le client (tenant) auquel elle appartient (par exemple acme). Il en faut au moins deux, de deux clients différents ; ajouter un deuxième compte du même client permet en plus de distinguer ce qui est partagé au sein d'un client.
Les secrets sont chiffrés dès l'enregistrement et ne sont plus jamais réaffichés : pour les changer, supprimez l'identité et recréez-la.
- Jeton Bearer
- Collez le jeton seul, sans le mot « Bearer ». Il est envoyé dans l'en-tête
Authorization. Préférez un jeton de longue durée, ou l'une des connexions ci-dessous si vos jetons expirent vite. - En-tête
- Un nom et une valeur, par exemple
X-Api-Key. - Cookie
- Le nom et la valeur du cookie de session.
- Connexion par identifiant et mot de passe
- Nous appelons votre endpoint de connexion au début de chaque scan. Indiquez son URL (un chemin comme
/api/login, ou une URL complète), le format du corps (JSON ou formulaire), les noms des champs (par défautemailetpassword) et, si la réponse contient un jeton, son chemin dans le JSON (par exempleaccess_tokenoudata.token). Sans chemin, nous cherchons un jeton usuel dans la réponse et réutilisons les cookies de session posés par la connexion. - OAuth 2
- URL https du serveur de jetons,
client_idet secret, avec le fluxclient_credentialsoupassword(identifiant et mot de passe de l'utilisateur de test). Portée (scope) et audience sont optionnelles.
Si votre API choisit le client actif par un en-tête ou un paramètre (voir les options), renseignez pour chaque identité la valeur de tenant qui lui correspond, par exemple acme.
Vérifier une identité
Le bouton Vérifier, à côté de chaque identité, envoie une seule requête à votre API avec ce compte (le premier endpoint de liste de la spec, ou { __typename } en GraphQL), en passant par la même connexion et les mêmes garde-fous qu'un scan. Vous savez en quelques secondes si le jeton est accepté, refusé (401, 403) ou si la connexion échoue, sans attendre un scan ni consommer votre quota. Trente vérifications par heure et par API.
Identifiants d'objets connus
Le scan trouve lui-même les objets de chaque compte en lisant les listes de l'API (/invoices, puis /invoices/{id}) et les références qu'elles contiennent. Un endpoint sans liste, ou dont les objets ne sont référencés nulle part, apparaît dans la couverture du scan comme sans identifiant. Donnez-lui alors des identifiants connus sur chaque identité, un par ligne :
contract_id = c-acme-1 report_id = r-acme-1
Le nom à gauche est celui du paramètre dans la spec ({contract_id}). Indiquez pour chaque compte des objets qui lui appartiennent : le scan tentera de les lire avec les comptes des autres clients.
Options du scan
- Exclusions
- Une expression régulière par ligne ; les chemins correspondants ne sont pas appelés. Par défaut, les déconnexions et l'authentification sont exclues pour ne pas invalider les sessions de test :
logout|signout|/auth/. - Chemins partagés
- Les données volontairement communes à tous les clients (catalogue, liste de pays, offres…) : une expression régulière par ligne. Une réponse identique pour deux clients n'y est pas signalée.
- Sélecteur de tenant
- Si un en-tête (par exemple
X-Tenant-Id) ou un paramètre de requête désigne le client actif, indiquez son nom : le scan vérifie que changer sa valeur ne donne pas accès à un autre client. - Accès anonyme
- Activé par défaut : chaque endpoint est aussi appelé sans authentification.
- Données sensibles
- Signale les secrets présents dans les réponses (empreintes de mots de passe, clés d'API, jetons). Ajoutez vos propres noms de champs sensibles, séparés par des virgules.
Lire les résultats
Chaque faille devient un problème, identifié par son type et son endpoint, et suivi d'un scan à l'autre. Sa page donne l'explication, les comptes concernés (qui a atteint les données de qui), la requête de reproduction en curl et la correction recommandée.
Gravité
- Critique
- Un client lit ou modifie les données d'un autre. À traiter immédiatement : c'est une violation de données potentielle.
- Élevée
- Un contrôle manque mais l'exploitation est plus limitée (accès anonyme, bascule de tenant, référence vers un objet d'un autre client…).
- Moyenne
- À vérifier à la main, par exemple un secret présent dans une réponse.
États
- Nouveau
- Trouvé pour la première fois par ce scan, ou réapparu après avoir été corrigé.
- Ouvert
- Toujours présent.
- Corrigé
- Le même test a été rejoué par un scan ultérieur et refusé. Un problème qui n'a simplement pas été retesté (endpoint exclu, scan interrompu) reste ouvert.
- Risque accepté
- Vous avez décidé de vivre avec, en expliquant pourquoi, éventuellement jusqu'à une date : à cette date, le problème redevient ouvert s'il est toujours présent.
- Faux positif
- Le comportement est voulu (donnée réellement partagée, par exemple). Pensez aussi aux chemins partagés.
Couverture
Un scan sans résultat ne vaut que par ce qu'il a réellement testé. L'onglet Couverture de chaque scan liste tous les endpoints décrits par la spec et dit, pour chacun, s'il a été testé (avec le nombre de refus et de fuites), s'il n'a pas pu l'être faute d'identifiant d'objet (voir les identifiants connus), ou pourquoi il a été ignoré : exclu par vos règles, méthode d'écriture sans tests d'écriture, déclaré public dans la spec.
Re-tester un problème
Une fois votre correctif déployé, le bouton Retester ce problème rejoue uniquement l'endpoint concerné avec vos comptes de test, en une minute environ. Les identifiants sont toujours collectés sur toute l'API, mais rien d'autre n'est sondé. Les re-tests ne comptent pas dans votre quota mensuel de scans ; ils sont limités par jour selon l'offre (3 en Découverte, 30 en Pro, 100 en Business). Le problème passe en « Corrigé » dès que le test rejoué est refusé.
Quand l'API change
Chaque scan complet compare ses endpoints à ceux du scan précédent et signale en tête de page les nouveaux endpoints (avec leur résultat : protégé, fuite, sans identifiant) et ceux qui ont disparu. La même information est reprise dans l'alerte e-mail et le webhook : un endpoint ajouté lundi par un développeur est testé mardi matin par le scan planifié, et vous le savez.
Créer un ticket
Sur la page d'un problème, « Créer un ticket » prépare un texte en Markdown (titre, endpoint, gravité, observation, impact, correction attendue, commande de reproduction, méthode de vérification) à coller dans Jira, GitHub, GitLab ou Linear.
Rapport
Chaque scan terminé propose un rapport en français, prêt à imprimer ou à enregistrer en PDF depuis le navigateur : verdict, couverture, chaque problème avec son observation, son impact et sa correction, les corrections confirmées, les risques acceptés, la méthode et les limites. C'est le document à transmettre à un client, un auditeur ou votre direction. Le rapport technique du moteur (en anglais), le JSON et le SARIF restent disponibles.
Planification et alertes
Avec les offres Pro et Business, chaque API peut être scannée chaque jour ou chaque semaine (le lundi), à l'heure de Paris de votre choix. Un scan planifié est ignoré si le précédent n'est pas terminé ou si le quota mensuel est atteint.
Quand un scan trouve un nouveau problème, en confirme la correction ou échoue :
- les membres qui ont laissé les notifications activées dans leur compte reçoivent un e-mail ;
- un message est posté sur le webhook de l'API, si vous en avez indiqué un (bouton « Envoyer un message de test » pour vérifier).
Webhooks
Deux formats, au choix dans les réglages de l'API :
- Texte : un message lisible, accepté tel quel par les webhooks entrants de Slack, Microsoft Teams, Mattermost et Rocket.Chat (
{"text": …}) et par Discord ({"content": …}). - JSON signé : pour votre propre intégration (création de ticket, blocage d'un déploiement, tableau de bord). Chaque événement est un POST JSON avec les en-têtes
X-Tenantleak-Event(scan.finished,scan.failedoutest) etX-Tenantleak-Signature: t=<horodatage>,v1=<signature>: HMAC-SHA256, avec la clé de signature affichée aux administrateurs, de la chaîne<horodatage>.<corps brut>. L'horodatage (secondes Unix) est couvert par la signature : refusez les événements de plus de cinq minutes, un événement capturé ne peut alors pas être rejoué. La clé se change en un clic.
{
"event": "scan.finished",
"sent_at": "2026-10-21T06:12:40Z",
"project": {"id": "…", "name": "API facturation (préprod)", "base_url": "https://api.staging.exemple.fr"},
"scan": {"id": "…", "number": 42, "status": "done", "trigger": "schedule", "requests": 318, "tested": 27,
"url": "…/app/projects/…/scans/…", "report_url": "…/rapport"},
"issues": {"new": 1, "still_open": 2, "fixed": 1},
"endpoints": {"new": [{"key": "GET /api/v2/invoices/{id}", "status": "tested", "leak": true}], "removed": [], "since": 41},
"findings": [{"id": "…", "state": "new", "kind": "cross_tenant_read", "title": "Lecture des données d'un autre client",
"severity": "high", "method": "GET", "path": "/api/v2/invoices/{id}", "url": "…"}]
}import hashlib, hmac, time
parts = dict(p.split("=", 1) for p in request.headers["X-Tenantleak-Signature"].split(","))
assert abs(time.time() - int(parts["t"])) < 300 # pas de rejeu d'un vieil événement
expected = hmac.new(SECRET.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, parts["v1"])Un scan sans changement n'envoie rien. Le lundi matin, les propriétaires et administrateurs reçoivent un résumé de la semaine (problèmes ouverts, nouveaux, corrigés, couverture, prochain scan), seulement s'il s'est passé quelque chose.
Les rapports détaillés d'un scan (couverture, rapport technique, JSON, SARIF) sont conservés 90 jours ; le résumé et les problèmes restent tant que l'API existe.
Sécurité du compte
La double authentification (application TOTP : Google Authenticator, 1Password, Bitwarden…) s'active depuis « Mon compte ». Huit codes de récupération à usage unique sont affichés une seule fois : rangez-les dans votre gestionnaire de mots de passe, ils remplacent l'application si vous perdez votre téléphone. Un propriétaire peut exiger la double authentification de toute l'équipe depuis la page Équipe. En dernier recours, écrivez-nous depuis votre adresse de compte pour une réinitialisation.
Connexion SSO (OpenID Connect, offre Business)
Un propriétaire relie le fournisseur d'identité de l'entreprise depuis la page Équipe : URL de l'émetteur (celle qui répond à /.well-known/openid-configuration), client_id, client_secret et domaine des adresses e-mail. Chez le fournisseur, déclarez une application web confidentielle avec l'adresse de retour https://tenantleak.redsentinel.fr/login/sso/callback et les portées openid email profile. Fonctionne avec Google Workspace, Microsoft Entra ID, Okta, Keycloak, Authentik et tout fournisseur OpenID Connect ; le flux utilise PKCE et vérifie la signature du jeton d'identité.
Le domaine des adresses doit d'abord être prouvé dans « Domaines », comme un hôte d'API (enregistrement DNS TXT _tenantleak.exemple.fr ou fichier /.well-known/tenantleak-verification.txt) : une adresse e-mail sur un domaine ne prouve pas qu'on l'administre. La preuve est reconfirmée tous les trente jours ; si elle disparaît, le SSO est suspendu jusqu'à nouvelle vérification. Les messageries grand public (gmail.com, outlook.fr…) sont refusées.
Les personnes dont l'adresse est sur ce domaine se connectent par « Se connecter avec le SSO de mon entreprise » : la première connexion crée leur compte et les ajoute comme membre, et le compte reste lié à l'identifiant (sub) que le fournisseur a présenté. Le second facteur est celui du fournisseur ; un compte qui existait déjà avec sa propre double authentification la confirme une fois, lors du premier rattachement. La connexion ne s'achève que dans le navigateur qui l'a commencée. Une fois le test réussi, l'option « Imposer le SSO » refuse la connexion par mot de passe aux membres de l'organisation et l'inscription par mot de passe aux nouvelles adresses du domaine ; elle ne touche pas les comptes du même domaine qui appartiennent à une autre organisation.
Référence de l'API
Disponible avec les offres Pro et Business. Créez un jeton dans Intégration CI : il commence par tlc_, n'est affiché qu'une fois et donne accès aux API de votre organisation seulement. Rangez-le dans les secrets de votre CI, jamais dans le dépôt.
Toutes les requêtes portent l'en-tête Authorization: Bearer tlc_… et renvoient du JSON. Limite : 120 requêtes par minute et par jeton.
| Requête | Rôle |
|---|---|
GET /api/v1/projects | Les API de l'organisation, avec leurs problèmes ouverts et leur dernier scan. |
GET /api/v1/projects/{id} | Une API. |
POST /api/v1/projects/{id}/scans | Lance un scan (réponse 202). Corps JSON facultatif : {"unsafe": true} pour inclure les tests d'écriture, s'ils sont activés. |
GET /api/v1/projects/{id}/scans?limit=20 | Les derniers scans d'une API. |
GET /api/v1/scans/{id}?fail_on=new | État du scan, problèmes trouvés et verdict gate. |
POST /api/v1/scans/{id}/cancel | Annule un scan en file ou en cours. |
GET /api/v1/scans/{id}/sarif | Le rapport SARIF 2.1.0. |
GET /api/v1/scans/{id}/report | Le rapport JSON complet. |
GET /api/v1/projects/{id}/issues?status=open | Les problèmes : open, fixed, triaged ou all. |
POST /api/v1/projects/{id}/issues/{issue}/retest | Rejoue uniquement l'endpoint de ce problème (réponse 202, même suivi qu'un scan). Hors quota mensuel. Pour un problème d'écriture, le corps {"unsafe": true} est obligatoire. |
Lancer un scan
L'identifiant de l'API figure dans l'adresse de sa page (/app/projects/<id>) et dans la page Intégration CI.
curl -X POST \ -H "Authorization: Bearer $TLC_TOKEN" \ "https://tenantleak.redsentinel.fr/api/v1/projects/$TLC_PROJECT/scans"
{
"id": "6f1c0e9a…",
"number": 14,
"status": "queued",
"trigger": "api",
"status_url": "https://tenantleak.redsentinel.fr/api/v1/scans/6f1c0e9a…",
"url": "https://tenantleak.redsentinel.fr/app/projects/…/scans/6f1c0e9a…"
}Attendre le verdict
Interrogez status_url toutes les 15 secondes environ. Tant que le scan tourne, gate.result vaut pending, puis :
pass- Rien qui doive bloquer selon
fail_on. failgate.failingproblèmes bloquants.error- Le scan a échoué ou a été annulé ; la raison est dans
gate.reason.
Le paramètre fail_on choisit ce qui fait échouer : new (par défaut : problèmes nouveaux ou réapparus dans ce scan), open (tout problème encore ouvert), critical (problèmes critiques ouverts) ou none (jamais, sauf erreur). Les problèmes en risque accepté ou faux positif ne bloquent jamais.
{
"id": "6f1c0e9a…",
"number": 14,
"status": "done",
"issues": {"new": 1, "still_open": 2, "fixed": 1},
"findings": [
{
"title": "Lecture des données d'un autre client",
"severity": "critical",
"state": "new",
"method": "GET",
"path": "/api/invoices/{id}",
"probes": ["bob_globex → alice_acme"],
"url": "https://tenantleak.redsentinel.fr/app/projects/…/issues/…"
}
],
"gate": {"result": "fail", "fail_on": "new", "failing": 1, "partial": false},
"reports": {"sarif": "https://tenantleak.redsentinel.fr/api/v1/scans/6f1c0e9a…/sarif", "report": "…"}
}Erreurs
Les erreurs renvoient {"error": "…"} avec le code HTTP correspondant :
401- Jeton absent, mal formé ou révoqué.
403- L'offre de l'organisation n'inclut pas l'API.
404- API ou scan inconnu pour cette organisation.
409- Le scan ne peut pas démarrer : un scan est déjà en cours, quota atteint, ou configuration incomplète. Le message dit lequel.
429- Plus de 120 requêtes en une minute : attendez la durée indiquée par
Retry-After.
Intégration continue
Le principe est le même partout : lancer le scan, interroger son état jusqu'à obtenir un verdict, puis faire échouer le job si le verdict est fail ou error.
GitHub Actions
Le plus court : l'action publiée avec le moteur open source. Ajoutez le secret TLC_TOKEN au dépôt ; le job échoue sur une nouvelle faille et le rapport SARIF apparaît dans l'onglet Security.
- uses: Chechene953/tenantleak/cloud-action@v0
with:
token: ${{ secrets.TLC_TOKEN }}
project: ID_DE_L_API # page « Intégration CI »
fail-on: new # new, open, critical ou none
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: tenantleak.sarifOu, sans action, le même enchaînement écrit à la main :
# .github/workflows/tenantleak.yml
name: tenantleak
on:
push:
branches: [main]
workflow_dispatch:
jobs:
tenantleak:
runs-on: ubuntu-latest
timeout-minutes: 40
permissions:
contents: read
security-events: write
env:
TLC_URL: https://tenantleak.redsentinel.fr
TLC_PROJECT: "identifiant-de-l-api"
TLC_TOKEN: ${{ secrets.TLC_TOKEN }}
steps:
- name: Lancer le scan
id: start
run: |
resp=$(curl -fsS -X POST -H "Authorization: Bearer $TLC_TOKEN" \
"$TLC_URL/api/v1/projects/$TLC_PROJECT/scans")
echo "scan=$(echo "$resp" | jq -r .id)" >> "$GITHUB_OUTPUT"
echo "Suivi : $(echo "$resp" | jq -r .url)"
- name: Attendre le verdict (30 minutes au plus)
id: wait
run: |
result=pending
for i in $(seq 1 120); do
resp=$(curl -fsS -H "Authorization: Bearer $TLC_TOKEN" \
"$TLC_URL/api/v1/scans/${{ steps.start.outputs.scan }}?fail_on=new")
result=$(echo "$resp" | jq -r .gate.result)
[ "$result" != "pending" ] && break
sleep 15
done
echo "$resp" | jq '{status, issues, gate}'
echo "result=$result" >> "$GITHUB_OUTPUT"
- name: Télécharger le rapport SARIF
if: always() && steps.start.outputs.scan != ''
run: |
curl -fsS -H "Authorization: Bearer $TLC_TOKEN" -o tenantleak.sarif \
"$TLC_URL/api/v1/scans/${{ steps.start.outputs.scan }}/sarif" || true
- name: Publier dans GitHub Security
if: always() && hashFiles('tenantleak.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: tenantleak.sarif
category: tenantleak
- name: Verdict
run: |
case "${{ steps.wait.outputs.result }}" in
pass) echo "Aucune nouvelle faille entre clients." ;;
fail) echo "Nouvelle faille entre clients : voir le scan."; exit 1 ;;
*) echo "Scan en échec ou non terminé."; exit 1 ;;
esacGitLab CI
Déclarez TLC_TOKEN comme variable CI/CD masquée.
# .gitlab-ci.yml
tenantleak:
stage: test
image: alpine:3.20
variables:
TLC_URL: "https://tenantleak.redsentinel.fr"
TLC_PROJECT: "identifiant-de-l-api"
before_script:
- apk add --no-cache curl jq
script:
- |
scan=$(curl -fsS -X POST -H "Authorization: Bearer $TLC_TOKEN" \
"$TLC_URL/api/v1/projects/$TLC_PROJECT/scans" | jq -r .id)
result=pending
for i in $(seq 1 120); do
resp=$(curl -fsS -H "Authorization: Bearer $TLC_TOKEN" \
"$TLC_URL/api/v1/scans/$scan?fail_on=new")
result=$(echo "$resp" | jq -r .gate.result)
[ "$result" != "pending" ] && break
sleep 15
done
echo "$resp" | jq '{status, issues, gate}'
test "$result" = "pass"Notre robot
Toutes les requêtes de scan, de téléchargement de spec et de vérification de domaine portent l'agent :
User-Agent: tenantleak-cloud/1.3 (+https://tenantleak.redsentinel.fr/bot)
Elles respectent 5 requêtes par seconde au plus, 5 000 requêtes et 15 minutes par scan, en lecture seule sauf activation des tests d'écriture. Si votre pare-feu applicatif bloque les scans, autorisez cet agent sur votre préproduction. Plus d'informations pour les administrateurs sur la page Notre robot.
Une question qui n'est pas traitée ici ? Écrivez à alex@redsentinel.fr.