Skip to content

Commit cc1f0b8

Browse files
authored
Merge pull request #1148 from vmware-tanzu/ldap_group_search_escape
Escape special characters in LDAP DNs when used in search filters
2 parents ee881aa + 2ad181c commit cc1f0b8

File tree

2 files changed

+98
-7
lines changed

2 files changed

+98
-7
lines changed

internal/upstreamldap/upstreamldap.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -715,18 +715,24 @@ func (p *Provider) groupSearchRequestedAttributes() []string {
715715
}
716716

717717
func (p *Provider) userSearchFilter(username string) string {
718-
safeUsername := p.escapeUsernameForSearchFilter(username)
718+
// The username is end user input, so it should be escaped before being included in a search to prevent
719+
// query injection.
720+
safeUsername := p.escapeForSearchFilter(username)
719721
if len(p.c.UserSearch.Filter) == 0 {
720722
return fmt.Sprintf("(%s=%s)", p.c.UserSearch.UsernameAttribute, safeUsername)
721723
}
722724
return interpolateSearchFilter(p.c.UserSearch.Filter, safeUsername)
723725
}
724726

725727
func (p *Provider) groupSearchFilter(userDN string) string {
728+
// The DN can contain characters that are considered special characters by LDAP searches, so it should be
729+
// escaped before being included in the search filter to prevent bad search syntax.
730+
// E.g. for the DN `CN=My User (Admin),OU=Users,OU=my,DC=my,DC=domain` we must escape the parens.
731+
safeUserDN := p.escapeForSearchFilter(userDN)
726732
if len(p.c.GroupSearch.Filter) == 0 {
727-
return fmt.Sprintf("(member=%s)", userDN)
733+
return fmt.Sprintf("(member=%s)", safeUserDN)
728734
}
729-
return interpolateSearchFilter(p.c.GroupSearch.Filter, userDN)
735+
return interpolateSearchFilter(p.c.GroupSearch.Filter, safeUserDN)
730736
}
731737

732738
func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) string {
@@ -737,9 +743,8 @@ func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string)
737743
return "(" + filter + ")"
738744
}
739745

740-
func (p *Provider) escapeUsernameForSearchFilter(username string) string {
741-
// The username is end user input, so it should be escaped before being included in a search to prevent query injection.
742-
return ldap.EscapeFilter(username)
746+
func (p *Provider) escapeForSearchFilter(s string) string {
747+
return ldap.EscapeFilter(s)
743748
}
744749

745750
// Returns the (potentially) binary data of the attribute's value, base64 URL encoded.

internal/upstreamldap/upstreamldap_test.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ func TestEndUserAuthentication(t *testing.T) {
479479
wantAuthResponse: expectedAuthResponse(nil),
480480
},
481481
{
482-
name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter, because the username is end-user input",
482+
name: "when the username has special LDAP search filter characters then they must be properly escaped in the custom user search filter, because the username is end-user input",
483483
username: `a&b|c(d)e\f*g`,
484484
password: testUpstreamPassword,
485485
providerConfig: providerConfig(nil),
@@ -497,6 +497,92 @@ func TestEndUserAuthentication(t *testing.T) {
497497
},
498498
wantAuthResponse: expectedAuthResponse(nil),
499499
},
500+
{
501+
name: "when the username has special LDAP search filter characters then they must be properly escaped in the default user search filter, because the username is end-user input",
502+
username: `a&b|c(d)e\f*g`,
503+
password: testUpstreamPassword,
504+
providerConfig: providerConfig(func(p *ProviderConfig) {
505+
p.UserSearch.Filter = ""
506+
}),
507+
searchMocks: func(conn *mockldapconn.MockConn) {
508+
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
509+
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
510+
r.Filter = fmt.Sprintf("(some-upstream-username-attribute=%s)", `a&b|c\28d\29e\5cf\2ag`)
511+
})).Return(exampleUserSearchResult, nil).Times(1)
512+
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
513+
Return(exampleGroupSearchResult, nil).Times(1)
514+
conn.EXPECT().Close().Times(1)
515+
},
516+
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
517+
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
518+
},
519+
wantAuthResponse: expectedAuthResponse(nil),
520+
},
521+
{
522+
name: "when the user search result DN has special LDAP search filter characters then they must be properly escaped in the custom group search filter",
523+
username: testUpstreamUsername,
524+
password: testUpstreamPassword,
525+
providerConfig: providerConfig(nil),
526+
searchMocks: func(conn *mockldapconn.MockConn) {
527+
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
528+
conn.EXPECT().Search(expectedUserSearch(nil)).
529+
Return(&ldap.SearchResult{
530+
Entries: []*ldap.Entry{
531+
{
532+
DN: `result DN with * \ special characters ()`,
533+
Attributes: []*ldap.EntryAttribute{
534+
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
535+
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
536+
},
537+
},
538+
},
539+
}, nil).Times(1)
540+
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
541+
escapedDN := `result DN with \2a \5c special characters \28\29`
542+
r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", escapedDN, escapedDN)
543+
}), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1)
544+
conn.EXPECT().Close().Times(1)
545+
},
546+
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
547+
conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1)
548+
},
549+
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
550+
r.DN = `result DN with * \ special characters ()`
551+
}),
552+
},
553+
{
554+
name: "when the user search result DN has special LDAP search filter characters then they must be properly escaped in the default group search filter",
555+
username: testUpstreamUsername,
556+
password: testUpstreamPassword,
557+
providerConfig: providerConfig(func(p *ProviderConfig) {
558+
p.GroupSearch.Filter = ""
559+
}),
560+
searchMocks: func(conn *mockldapconn.MockConn) {
561+
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
562+
conn.EXPECT().Search(expectedUserSearch(nil)).
563+
Return(&ldap.SearchResult{
564+
Entries: []*ldap.Entry{
565+
{
566+
DN: `result DN with * \ special characters ()`,
567+
Attributes: []*ldap.EntryAttribute{
568+
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
569+
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
570+
},
571+
},
572+
},
573+
}, nil).Times(1)
574+
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
575+
r.Filter = fmt.Sprintf("(member=%s)", `result DN with \2a \5c special characters \28\29`)
576+
}), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1)
577+
conn.EXPECT().Close().Times(1)
578+
},
579+
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
580+
conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1)
581+
},
582+
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
583+
r.DN = `result DN with * \ special characters ()`
584+
}),
585+
},
500586
{
501587
name: "group names are sorted to make the result more stable/predictable",
502588
username: testUpstreamUsername,

0 commit comments

Comments
 (0)