Skip to content

Commit 94b9449

Browse files
authored
xds/rbac: enforce strict presence-based short-circuit in authenticatedMatcher (#9111)
This PR fixes a bug in the xDS RBAC authenticatedMatcher where it incorrectly falls through URI/DNS SANs to the Subject DN. According to gRFC A41 and Envoy's specification, only the first non-empty identity source must be consulted. This bug allows an authorization bypass in certain scenarios. A regression test is included. RELEASE NOTES: - xds/rbac: Fix `Authenticated` matcher to use the first non-empty identity source (URI SAN, DNS SAN, and Subject DN) in the order specified in gRFC A41.
1 parent 89fe783 commit 94b9449

2 files changed

Lines changed: 123 additions & 14 deletions

File tree

internal/xds/rbac/matchers.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -435,17 +435,23 @@ func (am *authenticatedMatcher) match(data *rpcData) bool {
435435
return am.stringMatcher.Match("")
436436
}
437437
cert := data.certs[0]
438-
// The order of matching as per the RBAC documentation (see package-level comments)
439-
// is as follows: URI SANs, DNS SANs, and then subject name.
440-
for _, uriSAN := range cert.URIs {
441-
if am.stringMatcher.Match(uriSAN.String()) {
442-
return true
438+
// Use the first non-empty identity source in priority order:
439+
// URI SANs, then DNS SANs, then Subject.
440+
if len(cert.URIs) > 0 {
441+
for _, uriSAN := range cert.URIs {
442+
if am.stringMatcher.Match(uriSAN.String()) {
443+
return true
444+
}
443445
}
446+
return false
444447
}
445-
for _, dnsSAN := range cert.DNSNames {
446-
if am.stringMatcher.Match(dnsSAN) {
447-
return true
448+
if len(cert.DNSNames) > 0 {
449+
for _, dnsSAN := range cert.DNSNames {
450+
if am.stringMatcher.Match(dnsSAN) {
451+
return true
452+
}
448453
}
454+
return false
449455
}
450456
return am.stringMatcher.Match(cert.Subject.String())
451457
}

internal/xds/rbac/rbac_engine_test.go

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,8 +1111,8 @@ func (s) TestChainEngine(t *testing.T) {
11111111
},
11121112
},
11131113
},
1114-
// This test tests that when there are no SANs or Subject's
1115-
// distinguished name in incoming RPC's, that authenticated matchers
1114+
// This test verifies that when there are no SANs or Subject
1115+
// distinguished name in incoming RPCs, authenticated matchers
11161116
// match against the empty string.
11171117
{
11181118
name: "default-matching-no-credentials",
@@ -1131,10 +1131,9 @@ func (s) TestChainEngine(t *testing.T) {
11311131
},
11321132
},
11331133
rbacQueries: []rbacQuery{
1134-
// This incoming RPC Call should match with the service admin
1135-
// policy. No authentication info is provided, so the
1136-
// authenticated matcher should match to the string matcher on
1137-
// the empty string, matching to the service-admin policy.
1134+
// This incoming RPC should match the service admin policy. No
1135+
// authentication info is provided, so the authenticated matcher
1136+
// evaluates the empty string.
11381137
{
11391138
rpcData: &rpcData{
11401139
fullMethod: "some method",
@@ -1156,6 +1155,18 @@ func (s) TestChainEngine(t *testing.T) {
11561155
},
11571156
},
11581157
},
1158+
wantStatusCode: codes.PermissionDenied,
1159+
},
1160+
{
1161+
rpcData: &rpcData{
1162+
fullMethod: "some method",
1163+
peerInfo: &peer.Peer{
1164+
Addr: &addr{ipAddress: "0.0.0.0:8080"},
1165+
AuthInfo: credentials.TLSInfo{
1166+
State: tls.ConnectionState{},
1167+
},
1168+
},
1169+
},
11591170
wantStatusCode: codes.OK,
11601171
},
11611172
},
@@ -1932,3 +1943,95 @@ func createXDSTypedStruct(t *testing.T, in map[string]any, name string) *anypb.A
19321943
}
19331944
return customConfig
19341945
}
1946+
1947+
func TestAuthenticatedMatcherIdentitySourcePrecedence(t *testing.T) {
1948+
tests := []struct {
1949+
name string
1950+
uris []string
1951+
dnsNames []string
1952+
subjectCN string
1953+
matcherContains string
1954+
wantMatch bool
1955+
}{
1956+
{
1957+
name: "uriSANPresentAndMatches",
1958+
uris: []string{"spiffe://corp.example/svc/legitimate"},
1959+
matcherContains: "spiffe://corp.example/svc/legitimate",
1960+
wantMatch: true,
1961+
},
1962+
{
1963+
name: "uriSANPresentDoesNotFallbackToDNS",
1964+
uris: []string{"spiffe://corp.example/svc/other"},
1965+
dnsNames: []string{"spiffe://corp.example/svc/legitimate"},
1966+
matcherContains: "spiffe://corp.example/svc/legitimate",
1967+
wantMatch: false,
1968+
},
1969+
{
1970+
name: "uriSANPresentDoesNotFallbackToSubject",
1971+
uris: []string{"spiffe://corp.example/svc/other"},
1972+
subjectCN: "spiffe://corp.example/svc/legitimate",
1973+
matcherContains: "spiffe://corp.example/svc/legitimate",
1974+
wantMatch: false,
1975+
},
1976+
{
1977+
name: "dnsSANPresentAndMatchesWhenNoURI",
1978+
dnsNames: []string{"svc.legitimate.internal"},
1979+
matcherContains: "svc.legitimate.internal",
1980+
wantMatch: true,
1981+
},
1982+
{
1983+
name: "dnsSANPresentDoesNotFallbackToSubject",
1984+
dnsNames: []string{"svc.other.internal"},
1985+
subjectCN: "svc.legitimate.internal",
1986+
matcherContains: "svc.legitimate.internal",
1987+
wantMatch: false,
1988+
},
1989+
{
1990+
name: "subjectUsedWhenNoURINorDNS",
1991+
subjectCN: "svc.legitimate.internal",
1992+
matcherContains: "svc.legitimate.internal",
1993+
wantMatch: true,
1994+
},
1995+
}
1996+
1997+
for _, tt := range tests {
1998+
t.Run(tt.name, func(t *testing.T) {
1999+
var uriSANs []*url.URL
2000+
for _, u := range tt.uris {
2001+
parsed, err := url.Parse(u)
2002+
if err != nil {
2003+
t.Fatalf("url.Parse(%q) failed: %v", u, err)
2004+
}
2005+
uriSANs = append(uriSANs, parsed)
2006+
}
2007+
cert := &x509.Certificate{
2008+
URIs: uriSANs,
2009+
DNSNames: tt.dnsNames,
2010+
Subject: pkix.Name{
2011+
CommonName: tt.subjectCN,
2012+
},
2013+
}
2014+
2015+
amConfig := &v3rbacpb.Principal_Authenticated{
2016+
PrincipalName: &v3matcherpb.StringMatcher{
2017+
MatchPattern: &v3matcherpb.StringMatcher_Contains{
2018+
Contains: tt.matcherContains,
2019+
},
2020+
},
2021+
}
2022+
matcher, err := newAuthenticatedMatcher(amConfig)
2023+
if err != nil {
2024+
t.Fatalf("failed to create matcher: %v", err)
2025+
}
2026+
2027+
rpcData := &rpcData{
2028+
authType: "tls",
2029+
certs: []*x509.Certificate{cert},
2030+
}
2031+
2032+
if got := matcher.match(rpcData); got != tt.wantMatch {
2033+
t.Errorf("match() = %v, want %v", got, tt.wantMatch)
2034+
}
2035+
})
2036+
}
2037+
}

0 commit comments

Comments
 (0)