All advisories
Draft

SAN OTHER_NAME Truncation Bypasses Exact Match

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

SAN OTHER_NAME Truncation Bypasses Exact Match

Affected commit: c33d01624d
Sink: source/common/tls/cert_validator/san_matcher.cc:35

Summary

Envoy's mTLS certificate validator truncates a 256-character OBJECT-valued OTHER_NAME SAN to 255 characters before matching. A certificate with a different 256th character passes SAN-based authorization intended for a specific identity.

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 MAX_OID_LENGTH constant
echo "--- MAX_OID_LENGTH definition ---"
grep -n 'MAX_OID_LENGTH' source/common/tls/utility.cc

# Show the vulnerable truncation path
echo "--- OBJECT branch in generalNameAsString ---"
sed -n '314,321p' source/common/tls/utility.cc

The vulnerable code at utility.cc:314-320:

case V_ASN1_OBJECT: {
  char tmp_obj[MAX_OID_LENGTH];  // MAX_OID_LENGTH = 256
  int obj_len = OBJ_obj2txt(tmp_obj, MAX_OID_LENGTH, value->value.object, 1);
  if (obj_len > MAX_OID_LENGTH || obj_len < 0) {
    break;
  }
  san.assign(tmp_obj);  // truncated to 255 chars + NUL

OBJ_obj2txt writes at most MAX_OID_LENGTH bytes (255 chars + NUL terminator) but returns the full length needed. When an OBJECT OID string representation is exactly 256 characters long, obj_len == 256 == MAX_OID_LENGTH, so the guard obj_len > MAX_OID_LENGTH is false. Execution continues with san.assign(tmp_obj) which reads the NUL-terminated buffer — only 255 characters. The 256th character is silently lost.

A certificate with an OBJECT-valued OTHER_NAME SAN whose OID text representation differs only in its 256th character from the configured exact matcher will pass SAN-based authorization because both truncate to the same 255-character prefix.

Expected output:

HEAD: <resolved-commit>
--- MAX_OID_LENGTH definition ---
25:static constexpr int MAX_OID_LENGTH = 256;
315:      char tmp_obj[MAX_OID_LENGTH];
316:      int obj_len = OBJ_obj2txt(tmp_obj, MAX_OID_LENGTH, value->value.object, 1);
317:      if (obj_len > MAX_OID_LENGTH || obj_len < 0) {
--- OBJECT branch in generalNameAsString ---
    case V_ASN1_OBJECT: {
      char tmp_obj[MAX_OID_LENGTH];
      int obj_len = OBJ_obj2txt(tmp_obj, MAX_OID_LENGTH, value->value.object, 1);
      if (obj_len > MAX_OID_LENGTH || obj_len < 0) {
        break;
      }
      san.assign(tmp_obj);
      break;

Credit

Zheng Yu @ Depthfirst