Skip to content

[ZEPPELIN-6599] Stop treating the user list search text as LDAP filter and regex syntax - #5384

Open
kimyenac wants to merge 3 commits into
apache:masterfrom
kimyenac:ZEPPELIN-6599
Open

[ZEPPELIN-6599] Stop treating the user list search text as LDAP filter and regex syntax#5384
kimyenac wants to merge 3 commits into
apache:masterfrom
kimyenac:ZEPPELIN-6599

Conversation

@kimyenac

@kimyenac kimyenac commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What is this PR for?

GET /api/security/userlist/{searchText} interprets the client-supplied search text as syntax in two places. This PR fixes both.

1. LDAP search filters

ShiroAuthenticationService builds the LDAP search filter by string concatenation, in getUserList(DefaultLdapRealm, String, int) and getUserList(LdapRealm, String, int). The filter metacharacters (, ), *, \ and NUL are therefore interpreted as filter syntax instead of literal text.

Both sites now go through LdapFilterEncoder.escapeFilterValue, the RFC 4515 escape utility that ActiveDirectoryGroupRealm already uses. The filter building is extracted into two small helpers so the rendered filter can be asserted directly in unit tests, following the expandFilterTemplate approach already used in LdapRealm.

The wildcards that Zeppelin adds around the search text stay outside the escaped value, so substring matching behaves exactly as before. Only an asterisk typed by the user becomes a literal character. The configured attribute name and object class are escaped as well for defense in depth, while the raw attribute name is still used for setReturningAttributes.

The remaining realm branches of getMatchedUsers are already safe: ActiveDirectoryGroupRealm escapes the value in searchForUserName, and the JdbcRealm branch uses a PreparedStatement with a validated identifier.

2. Regular expression in the sorting comparator

SecurityRestApi sorted the matched users with o1.matches(searchText + "(.*)"), which compiles the search text as a regular expression. A search text of * therefore fails with PatternSyntaxException: Dangling meta character '*' and the endpoint responds with HTTP 500. That comparator only wants to list the users whose name starts with the search text first, which startsWith does without compiling anything. The replacement is a consistent comparator as well, and the alphabetical order within each group is preserved because the sort is stable.

What type of PR is it?

Improvement

Todos

  • Escape the search text at both LDAP filter building sites
  • Unit tests for the rendered filters (16)
  • Sort the user list without compiling the search text as a regular expression
  • Endpoint test for search texts containing regex metacharacters

What is the Jira issue?

https://issues.apache.org/jira/browse/ZEPPELIN-6599

How should this be tested?

./mvnw test -pl zeppelin-server -Dtest=ShiroAuthenticationServiceFilterInjectionTest,SecurityRestApiTest

SecurityRestApiTest.testGetUserListWithRegexMetacharacters returns HTTP 500 without the comparator change and HTTP 200 with it. The existing LDAP realm and Shiro service tests keep passing.

The LDAP part was also verified manually against an in-memory LDAP server. With the search text *)(cn=Bob, the directory server received (&(objectclass=person)(uid=*)(cn=Bob*)) before this change, so the uid condition was neutralized and an extra condition was appended. After this change the same input arrives as (&(objectclass=person)(uid=*\2a\29\28cn=Bob*)), while a normal search text produces exactly the same filter and the same results as before.

Worth noting that the entries matched by an injected filter are not disclosed in the REST response today, because SecurityRestApi filters the returned list once more with containsIgnoreCase(user, searchText). So the LDAP part is hardening of the filter building rather than a fix for an information leak.

Screenshots (if appropriate)

Not applicable.

Questions

  • Does the licenses files need update? No
  • Is there breaking changes for older versions? No
  • Does this needs documentation? No

@kimyenac kimyenac changed the title [ZEPPELIN-6599] Escape user-supplied search text in LDAP user search filters [ZEPPELIN-6599] Stop treating the user list search text as LDAP filter and regex syntax Aug 5, 2026
@jongyoul
jongyoul requested a lite review from Copilot August 5, 2026 02:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens GET /api/security/userlist/{searchText} by ensuring client-supplied search text is treated as literal text rather than LDAP filter / regex syntax, preventing filter injection and avoiding regex compilation errors in sorting.

Changes:

  • Escapes LDAP filter values when constructing LDAP search filters in ShiroAuthenticationService.
  • Replaces regex-based prefix prioritization in SecurityRestApi with a startsWith-based stable comparator.
  • Adds unit/integration tests covering LDAP filter rendering and userlist searches containing regex metacharacters.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
zeppelin-server/src/main/java/org/apache/zeppelin/service/ShiroAuthenticationService.java Routes LDAP filter construction through RFC 4515 escaping helpers.
zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java Removes regex usage from sorting; prioritizes prefix matches via startsWith.
zeppelin-server/src/test/java/org/apache/zeppelin/service/ShiroAuthenticationServiceFilterInjectionTest.java Adds tests asserting rendered LDAP filters don’t allow metacharacter injection.
zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java Adds endpoint test ensuring regex metacharacters don’t trigger HTTP 500.
Suppressed comments (1)

zeppelin-server/src/test/java/org/apache/zeppelin/service/ShiroAuthenticationServiceFilterInjectionTest.java:127

  • assertMetacharacterCounts currently verifies escapes for '(', ')' and '*', but not for backslash (\) and the NUL byte (\0). If those payloads are included, add assertions for the expected RFC 4515 escape sequences (\5c and \00).
    if (payload.indexOf('(') >= 0) {
      assertTrue(rendered.contains("\\28"), "missing \\28 in: " + rendered);
    }
    if (payload.indexOf(')') >= 0) {
      assertTrue(rendered.contains("\\29"), "missing \\29 in: " + rendered);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to +58
static Stream<String> injectionPayloads() {
return Stream.of(
")(uid=*",
"admin)(|(uid=*",
"*",
"admin)(cn=a*",
")(mail=*@corp.com",
"alice)(userPassword=*");
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added in c7310e0. The payload list now includes a bare backslash and a NUL, plus two payloads that look like already escaped sequences so double escaping is covered as well. assertMetacharacterCounts asserts the \5c and \00 escape sequences too, and a separate test pins the exact rendered filter for both characters since the parameterized display name cannot show them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants