All advisories
Draft

TLS Session Resumption Crosses Upstream Host Boundary

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

TLS Session Resumption Crosses Upstream Host Boundary

Affected commit: c33d01624d
Sink: source/common/tls/client_context_impl.cc:264

Summary

When SNI-scoped session caching is disabled, Envoy can resume a TLS session created for one upstream host while connecting to a different host. This skips fresh certificate validation and may allow traffic to cross origin or tenant boundaries.

Reproduce

#!/bin/bash
set -e

# Clone and identify HEAD
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-repro
cd /tmp/envoy-repro
echo "HEAD: $(git rev-parse HEAD)"

# Verify the context-wide cache path ignores SNI
echo "--- setSessionFromContextCache: no SNI check ---"
sed -n '255,270p' source/common/tls/client_context_impl.cc

# Verify the runtime guard controlling the fallback
echo "--- scopeUpstreamTlsSessionCacheBySni guard in newSessionKey ---"
sed -n '280,286p' source/common/tls/client_context_impl.cc

The vulnerable code at client_context_impl.cc:255-265:

void ClientContextImpl::setSessionFromContextCache(SSL* ssl) {
  absl::WriterMutexLock lock(session_keys_mu_);
  if (session_keys_.empty()) {
    return;
  }
  // Runtime-guarded rollback path for the previous context-wide cache
  // behavior. This deliberately ignores SNI and should only be used while the
  // reloadable feature remains available.
  SSL_SESSION* session = session_keys_.front().get();
  SSL_set_session(ssl, session);

When scopeUpstreamTlsSessionCacheBySni() returns false, newSessionKey stores sessions in a context-wide cache without SNI keys. setSessionFromContextCache then resumes the front session for any new connection regardless of which upstream host it targets. A session established to host-A.example.com is resumed when connecting to host-B.example.com, skipping fresh certificate validation. Traffic intended for one upstream tenant reaches another.

Expected output:

HEAD: <resolved-commit>
--- setSessionFromContextCache: no SNI check ---
void ClientContextImpl::setSessionFromContextCache(SSL* ssl) {
  absl::WriterMutexLock lock(session_keys_mu_);
  if (session_keys_.empty()) {
    return;
  }

  // Runtime-guarded rollback path for the previous context-wide cache
  // behavior. This deliberately ignores SNI and should only be used while the
  // reloadable feature remains available.
  SSL_SESSION* session = session_keys_.front().get();
  SSL_set_session(ssl, session);

  if (SSL_SESSION_should_be_single_use(session)) {
    session_keys_.pop_front();
  }
}
--- scopeUpstreamTlsSessionCacheBySni guard in newSessionKey ---
  if (!scopeUpstreamTlsSessionCacheBySni()) {
    absl::WriterMutexLock lock(session_keys_mu_);
    while (session_keys_.size() >= max_session_keys_) {
      session_keys_.pop_back();
    }

Credit

Zheng Yu @ Depthfirst