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