All advisories
Draft

XFCC URI SAN Injection via Unquoted Semicolons

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

XFCC URI SAN Injection via Unquoted Semicolons

Affected commit: c33d01624d
Sink: source/common/http/conn_manager_utility.cc:636

Summary

When forwarding URI SANs in text XFCC headers, Envoy emits URI=value without quoting, while using ; as the XFCC attribute delimiter. A certificate whose URI SAN contains a semicolon (e.g. spiffe://x.com/a;Subject="admin") creates a forged XFCC attribute. Downstream services that parse XFCC identity data would authorize a low-privilege mTLS client as a privileged user.

Detail

// source/common/http/conn_manager_utility.cc:636
absl::StrCat("URI=", uri)   // no quoting
// Compare with:
// line 629: Subject is quoted

Line 659 joins all details with ;. The XFCC specification requires values containing delimiters to be quoted, but URI= values are emitted bare.

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 URI= is emitted without quoting while Subject= is quoted
echo "--- URI emitted bare (no quotes) ---"
sed -n '631,637p' source/common/http/conn_manager_utility.cc

echo "--- Subject emitted with quotes ---"
sed -n '626,630p' source/common/http/conn_manager_utility.cc

echo "--- Joined with semicolon delimiter ---"
grep -n 'StrJoin.*client_cert_details.*";"' source/common/http/conn_manager_utility.cc

The vulnerable code at conn_manager_utility.cc:636:

case ClientCertDetailsType::URI: {
  const absl::Span<const std::string> sans = connection.ssl()->uriSanPeerCertificate();
  if (!sans.empty()) {
    for (const std::string& uri : sans) {
      client_cert_details.push_back(absl::StrCat("URI=", uri));  // no quoting
    }
  }

Compare with Subject at line 629 which uses quotes: absl::StrCat("Subject=\"", ..., "\"").

All details are joined with ; at line 659: client_cert_details_str = absl::StrJoin(client_cert_details, ";").

A client presenting a certificate with URI SAN spiffe://cluster.local/svc;Subject="admin" produces the XFCC header:

x-forwarded-client-cert: URI=spiffe://cluster.local/svc;Subject="admin"

A downstream service parsing this XFCC header sees two attributes: URI=spiffe://cluster.local/svc and Subject="admin". The attacker-injected Subject attribute can impersonate a privileged identity.

Expected output:

HEAD: <resolved-commit>
--- URI emitted bare (no quotes) ---
        case ClientCertDetailsType::URI: {
          // The "URI" key still exists even if the URI is empty.
          const absl::Span<const std::string> sans = connection.ssl()->uriSanPeerCertificate();
          if (!sans.empty()) {
            for (const std::string& uri : sans) {
              client_cert_details.push_back(absl::StrCat("URI=", uri));
            }
--- Subject emitted with quotes ---
        case ClientCertDetailsType::Subject:
          // The "Subject" key still exists even if the subject is empty.
          client_cert_details.push_back(
              absl::StrCat("Subject=\"", connection.ssl()->subjectPeerCertificate(), "\""));
          break;
--- Joined with semicolon delimiter ---
659:      client_cert_details_str = absl::StrJoin(client_cert_details, ";");

Credit

Zheng Yu @ Depthfirst