Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Escape special characters in LDAP DNs when used in search filters #1148

Merged
merged 2 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions internal/upstreamldap/upstreamldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,18 +715,24 @@ func (p *Provider) groupSearchRequestedAttributes() []string {
}

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

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

func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) string {
Expand All @@ -737,9 +743,8 @@ func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string)
return "(" + filter + ")"
}

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

// Returns the (potentially) binary data of the attribute's value, base64 URL encoded.
Expand Down
88 changes: 87 additions & 1 deletion internal/upstreamldap/upstreamldap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ func TestEndUserAuthentication(t *testing.T) {
wantAuthResponse: expectedAuthResponse(nil),
},
{
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",
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",
username: `a&b|c(d)e\f*g`,
password: testUpstreamPassword,
providerConfig: providerConfig(nil),
Expand All @@ -497,6 +497,92 @@ func TestEndUserAuthentication(t *testing.T) {
},
wantAuthResponse: expectedAuthResponse(nil),
},
{
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",
username: `a&b|c(d)e\f*g`,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.UserSearch.Filter = ""
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
r.Filter = fmt.Sprintf("(some-upstream-username-attribute=%s)", `a&b|c\28d\29e\5cf\2ag`)
})).Return(exampleUserSearchResult, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(nil),
},
{
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",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(nil),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: `result DN with * \ special characters ()`,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
escapedDN := `result DN with \2a \5c special characters \28\29`
r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", escapedDN, escapedDN)
}), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
r.DN = `result DN with * \ special characters ()`
}),
},
{
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",
username: testUpstreamUsername,
password: testUpstreamPassword,
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.Filter = ""
}),
searchMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: `result DN with * \ special characters ()`,
Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
r.Filter = fmt.Sprintf("(member=%s)", `result DN with \2a \5c special characters \28\29`)
}), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1)
},
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
r.DN = `result DN with * \ special characters ()`
}),
},
{
name: "group names are sorted to make the result more stable/predictable",
username: testUpstreamUsername,
Expand Down