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