Skip to content

Commit bb013c4

Browse files
Vilnius RamanauskasVarunNagaraju
authored andcommitted
PS-9704 [8.0]: Escape all RFC 4515 special chars in LDAP filter substitutions
https://perconadev.atlassian.net/browse/PS-9704 User-supplied values (user_name, user_dn) substituted into LDAP search filters via {UA} and {UD} were not properly escaped. Only double-quote received a partial treatment; parentheses, asterisk, backslash, and NUL were passed through verbatim, producing malformed filters that silently broke group lookups. Add ldap_filter_escape() which applies the mandatory RFC 4515 \XX hex encoding for NUL (\00), ( (\28), ) (\29), * (\2a), and \ (\5c). Apply it to all three substitution sites: search_dn() for user_name, and search_groups() for both {UA} (user_name) and {UD} (user_dn). MTR test exercises DN containing a double-quote, parentheses, an asterisk, and a backslash — all expected to receive their group role.
1 parent d1455d9 commit bb013c4

9 files changed

Lines changed: 351 additions & 5 deletions

plugin/auth_ldap/src/connection.cc

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,43 @@
1818
#include <iostream>
1919
#include <iterator>
2020
#include <regex>
21+
#include <sstream>
2122

2223
#include "plugin/auth_ldap/include/plugin_log.h"
2324

2425
namespace {
26+
27+
// RFC 4515 §3 requires these five octets to be escaped as \XX
28+
// when they appear in an LDAP filter assertion value:
29+
// NUL (0x00), LPAREN (0x28), RPAREN (0x29), ASTERISK (0x2a), ESC (0x5c)
30+
std::string ldap_filter_escape(const std::string &input) {
31+
std::ostringstream out;
32+
out << std::hex;
33+
for (unsigned char c : input) {
34+
switch (c) {
35+
case '\0':
36+
out << "\\00";
37+
break;
38+
case '(':
39+
out << "\\28";
40+
break;
41+
case ')':
42+
out << "\\29";
43+
break;
44+
case '*':
45+
out << "\\2a";
46+
break;
47+
case '\\':
48+
out << "\\5c";
49+
break;
50+
default:
51+
out << c;
52+
break;
53+
}
54+
}
55+
return out.str();
56+
}
57+
2558
// example of this callback is in the OpenLDAP's
2659
// servers/slapd/back-meta/bind.c (meta_back_default_urllist)
2760
int cb_urllist_proc(LDAP * /* ld */, LDAPURLDesc **urllist, LDAPURLDesc **url,
@@ -247,7 +280,7 @@ std::string Connection::search_dn(const std::string &user_name,
247280

248281
std::string str;
249282
std::ostringstream log_stream;
250-
std::string filter = user_search_attr + "=" + user_name;
283+
std::string filter = user_search_attr + "=" + ldap_filter_escape(user_name);
251284

252285
log_stream << "search_dn(" << base_dn << ", " << filter << ")";
253286
log_srv_dbg(log_stream.str());
@@ -306,10 +339,10 @@ groups_t Connection::search_groups(const std::string &user_name,
306339

307340
groups_t list;
308341
std::stringstream log_stream;
309-
std::string filter = std::regex_replace(group_search_filter,
310-
std::regex("\\{UA\\}"), user_name);
311-
std::string escaped_user_dn =
312-
std::regex_replace(user_dn, std::regex("\\\\\""), "\\\\\"");
342+
std::string escaped_user_name = ldap_filter_escape(user_name);
343+
std::string escaped_user_dn = ldap_filter_escape(user_dn);
344+
std::string filter = std::regex_replace(
345+
group_search_filter, std::regex("\\{UA\\}"), escaped_user_name);
313346
filter = std::regex_replace(filter, std::regex("\\{UD\\}"), escaped_user_dn);
314347

315348
LDAPMessage *l_result;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dn: cn=Test*User,ou=people,dc=planetexpress,dc=com
2+
objectClass: inetOrgPerson
3+
cn: Test*User
4+
sn: User
5+
uid: testasteriskuser
6+
userPassword: testpass123
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
$AUTH_LDAP_OPT $AUTH_LDAP_LOAD
2+
--authentication_ldap_simple_server_host='$MTR_LDAP_HOST'
3+
--authentication_ldap_simple_server_port=$MTR_LDAP_PORT
4+
--authentication_ldap_simple_fallback_server_host='$MTR_LDAP_FALLBACK_HOST'
5+
--authentication_ldap_simple_fallback_server_port=$MTR_LDAP_FALLBACK_PORT
6+
--authentication_ldap_simple_bind_root_pwd='GoodNewsEveryone'
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
SET GLOBAL authentication_ldap_simple_bind_base_dn = 'ou=people,dc=planetexpress,dc=com';
2+
SET GLOBAL authentication_ldap_simple_group_search_filter = '(&(objectClass=groupOfNames)(member={UD}))';
3+
CREATE ROLE 'test_dn_role';
4+
SET GLOBAL authentication_ldap_simple_group_role_mapping = 'dn_test_group=test_dn_role';
5+
SET GLOBAL authentication_ldap_simple_log_status = 6;
6+
CREATE USER normaluser IDENTIFIED WITH authentication_ldap_simple BY 'cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com';
7+
# Test 1: Normal user - role should be granted
8+
SHOW GRANTS FOR 'normaluser';
9+
Grants for normaluser@%
10+
GRANT USAGE ON *.* TO `normaluser`@`%`
11+
GRANT `test_dn_role`@`%` TO `normaluser`@`%`
12+
CREATE USER quoteduser IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test\\"User,ou=people,dc=planetexpress,dc=com';
13+
# Test 2: Quoted-DN user - role should be granted
14+
SHOW GRANTS FOR 'quoteduser';
15+
Grants for quoteduser@%
16+
GRANT USAGE ON *.* TO `quoteduser`@`%`
17+
GRANT `test_dn_role`@`%` TO `quoteduser`@`%`
18+
CREATE USER parentheses_user IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test(User),ou=people,dc=planetexpress,dc=com';
19+
# Test 3: Parentheses-DN user - role should be granted
20+
SHOW GRANTS FOR 'parentheses_user';
21+
Grants for parentheses_user@%
22+
GRANT USAGE ON *.* TO `parentheses_user`@`%`
23+
GRANT `test_dn_role`@`%` TO `parentheses_user`@`%`
24+
CREATE USER asterisk_user IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test*User,ou=people,dc=planetexpress,dc=com';
25+
# Test 4: Asterisk-DN user - role should be granted
26+
SHOW GRANTS FOR 'asterisk_user';
27+
Grants for asterisk_user@%
28+
GRANT USAGE ON *.* TO `asterisk_user`@`%`
29+
GRANT `test_dn_role`@`%` TO `asterisk_user`@`%`
30+
CREATE USER backslash_user IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test\\5cUser,ou=people,dc=planetexpress,dc=com';
31+
# Test 5: Backslash-DN user - role should be granted
32+
SHOW GRANTS FOR 'backslash_user';
33+
Grants for backslash_user@%
34+
GRANT USAGE ON *.* TO `backslash_user`@`%`
35+
GRANT `test_dn_role`@`%` TO `backslash_user`@`%`
36+
DROP USER normaluser;
37+
DROP USER quoteduser;
38+
DROP USER parentheses_user;
39+
DROP USER asterisk_user;
40+
DROP USER backslash_user;
41+
DROP ROLE test_dn_role;
42+
SET GLOBAL authentication_ldap_simple_bind_base_dn = '';
43+
SET GLOBAL authentication_ldap_simple_log_status = 1;
44+
SET GLOBAL authentication_ldap_simple_group_role_mapping = '';
45+
SET GLOBAL authentication_ldap_simple_group_search_filter = '(|(&(objectClass=posixGroup)(memberUid={UA}))(&(objectClass=group)(member={UD})))';
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Test: Validate LDAP group-role mapping when user DN contains characters
2+
# that are special in LDAP filter syntax (RFC 4515).
3+
#
4+
# Background:
5+
# The plugin substitutes {UD} with the authenticated user's DN in
6+
# authentication_ldap_simple_group_search_filter. Per RFC 4515 §3,
7+
# five ASCII octets must be escaped as \XX hex when they appear inside
8+
# an LDAP filter assertion value:
9+
# NUL (0x00), '(' (0x28), ')' (0x29), '*' (0x2a), '\' (0x5c)
10+
#
11+
# Purpose of this test:
12+
# Verify that role mapping works for all of these users in the same LDAP group:
13+
# 1. Normal DN (no special chars in CN)
14+
# 2. DN containing double quote (cn=Test"User)
15+
# 3. DN containing parentheses (cn=Test(User))
16+
# 4. DN containing asterisk (cn=Test*User)
17+
# 5. DN containing backslash (cn=Test\5cUser — LDAP-escaped form of Test\User)
18+
#
19+
# Expected result:
20+
# All five users authenticate successfully and are granted test_dn_role.
21+
#
22+
# Requires docker-test-openldap:
23+
# https://github.com/rroemhild/docker-test-openldap
24+
25+
--source include/count_sessions.inc
26+
27+
# -----------------------------------------------------------------
28+
# LDAP Setup: add users with special chars in CN and a groupOfNames group
29+
# -----------------------------------------------------------------
30+
--perl EOF
31+
use strict;
32+
use warnings;
33+
34+
sub run_cmd {
35+
my @cmd = @_;
36+
my $pid = fork();
37+
die "fork: $!" unless defined $pid;
38+
if ($pid == 0) {
39+
open(STDOUT, '>', '/dev/null');
40+
open(STDERR, '>', '/dev/null');
41+
exec(@cmd) or exit(127);
42+
}
43+
waitpid($pid, 0);
44+
return $? >> 8;
45+
}
46+
47+
my $host = $ENV{'MTR_LDAP_HOST'} || 'localhost';
48+
my $port = $ENV{'MTR_LDAP_PORT'} || '389';
49+
my $ldap_uri = "ldap://$host:$port";
50+
my $admin_dn = 'cn=admin,dc=planetexpress,dc=com';
51+
my $admin_pw = 'GoodNewsEveryone';
52+
my $test_dir = $ENV{'MYSQL_TEST_DIR'} . '/../plugin/auth_ldap/tests/mtr';
53+
my $user_ldif = "$test_dir/quote_test_user.ldif";
54+
my $paren_user_ldif = "$test_dir/paren_test_user.ldif";
55+
my $asterisk_user_ldif = "$test_dir/asterisk_test_user.ldif";
56+
my $backslash_user_ldif = "$test_dir/backslash_test_user.ldif";
57+
my $group_ldif = "$test_dir/test_group.ldif";
58+
59+
# Remove leftover entries from any previous failed run
60+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
61+
"-w", $admin_pw,
62+
'cn=dn_test_group,ou=people,dc=planetexpress,dc=com');
63+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
64+
"-w", $admin_pw,
65+
'cn=Test\"User,ou=people,dc=planetexpress,dc=com');
66+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
67+
"-w", $admin_pw,
68+
'cn=Test(User),ou=people,dc=planetexpress,dc=com');
69+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
70+
"-w", $admin_pw,
71+
'cn=Test*User,ou=people,dc=planetexpress,dc=com');
72+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
73+
"-w", $admin_pw,
74+
'cn=Test\5cUser,ou=people,dc=planetexpress,dc=com');
75+
76+
# Create the test entries
77+
my $rc;
78+
$rc = run_cmd("ldapadd", "-x", "-H", $ldap_uri, "-D", $admin_dn,
79+
"-w", $admin_pw, "-f", $user_ldif);
80+
die "ldapadd (quote user) failed rc=$rc" if $rc != 0;
81+
82+
$rc = run_cmd("ldapadd", "-x", "-H", $ldap_uri, "-D", $admin_dn,
83+
"-w", $admin_pw, "-f", $paren_user_ldif);
84+
die "ldapadd (paren user) failed rc=$rc" if $rc != 0;
85+
86+
$rc = run_cmd("ldapadd", "-x", "-H", $ldap_uri, "-D", $admin_dn,
87+
"-w", $admin_pw, "-f", $asterisk_user_ldif);
88+
die "ldapadd (asterisk user) failed rc=$rc" if $rc != 0;
89+
90+
$rc = run_cmd("ldapadd", "-x", "-H", $ldap_uri, "-D", $admin_dn,
91+
"-w", $admin_pw, "-f", $backslash_user_ldif);
92+
die "ldapadd (backslash user) failed rc=$rc" if $rc != 0;
93+
94+
$rc = run_cmd("ldapadd", "-x", "-H", $ldap_uri, "-D", $admin_dn,
95+
"-w", $admin_pw, "-f", $group_ldif);
96+
die "ldapadd (group) failed rc=$rc" if $rc != 0;
97+
EOF
98+
99+
# -----------------------------------------------------------------
100+
# MySQL Setup
101+
# -----------------------------------------------------------------
102+
SET GLOBAL authentication_ldap_simple_bind_base_dn = 'ou=people,dc=planetexpress,dc=com';
103+
104+
# Use a DN-based filter so {UD} substitution is exercised
105+
SET GLOBAL authentication_ldap_simple_group_search_filter = '(&(objectClass=groupOfNames)(member={UD}))';
106+
107+
CREATE ROLE 'test_dn_role';
108+
SET GLOBAL authentication_ldap_simple_group_role_mapping = 'dn_test_group=test_dn_role';
109+
110+
SET GLOBAL authentication_ldap_simple_log_status = 6;
111+
112+
# -----------------------------------------------------------------
113+
# Test 1: Normal user whose DN has no special characters.
114+
# Expected: role IS granted.
115+
# -----------------------------------------------------------------
116+
CREATE USER normaluser IDENTIFIED WITH authentication_ldap_simple BY 'cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com';
117+
118+
--connect (con_normal,localhost,normaluser,hermes,,,,CLEARTEXT)
119+
--echo # Test 1: Normal user - role should be granted
120+
SHOW GRANTS FOR 'normaluser';
121+
--disconnect con_normal
122+
--connection default
123+
124+
# -----------------------------------------------------------------
125+
# Test 2: User whose DN contains a double-quote.
126+
# Expected: role IS granted.
127+
# -----------------------------------------------------------------
128+
CREATE USER quoteduser IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test\\"User,ou=people,dc=planetexpress,dc=com';
129+
130+
--connect (con_quoted,localhost,quoteduser,testpass123,,,,CLEARTEXT)
131+
--echo # Test 2: Quoted-DN user - role should be granted
132+
SHOW GRANTS FOR 'quoteduser';
133+
--disconnect con_quoted
134+
--connection default
135+
136+
# -----------------------------------------------------------------
137+
# Test 3: User whose DN contains parentheses.
138+
# Expected: role IS granted.
139+
# -----------------------------------------------------------------
140+
CREATE USER parentheses_user IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test(User),ou=people,dc=planetexpress,dc=com';
141+
142+
--connect (con_parentheses,localhost,parentheses_user,testpass123,,,,CLEARTEXT)
143+
--echo # Test 3: Parentheses-DN user - role should be granted
144+
SHOW GRANTS FOR 'parentheses_user';
145+
--disconnect con_parentheses
146+
--connection default
147+
148+
# -----------------------------------------------------------------
149+
# Test 4: User whose DN contains an asterisk.
150+
# Expected: role IS granted.
151+
# -----------------------------------------------------------------
152+
CREATE USER asterisk_user IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test*User,ou=people,dc=planetexpress,dc=com';
153+
154+
--connect (con_asterisk,localhost,asterisk_user,testpass123,,,,CLEARTEXT)
155+
--echo # Test 4: Asterisk-DN user - role should be granted
156+
SHOW GRANTS FOR 'asterisk_user';
157+
--disconnect con_asterisk
158+
--connection default
159+
160+
# -----------------------------------------------------------------
161+
# Test 5: User whose DN contains a backslash.
162+
# The LDAP DN uses \5c to represent a literal backslash in the CN.
163+
# Expected: role IS granted.
164+
# -----------------------------------------------------------------
165+
CREATE USER backslash_user IDENTIFIED WITH authentication_ldap_simple BY 'cn=Test\\5cUser,ou=people,dc=planetexpress,dc=com';
166+
167+
--connect (con_backslash,localhost,backslash_user,testpass123,,,,CLEARTEXT)
168+
--echo # Test 5: Backslash-DN user - role should be granted
169+
SHOW GRANTS FOR 'backslash_user';
170+
--disconnect con_backslash
171+
--connection default
172+
173+
# -----------------------------------------------------------------
174+
# MySQL Cleanup
175+
# -----------------------------------------------------------------
176+
DROP USER normaluser;
177+
DROP USER quoteduser;
178+
DROP USER parentheses_user;
179+
DROP USER asterisk_user;
180+
DROP USER backslash_user;
181+
DROP ROLE test_dn_role;
182+
SET GLOBAL authentication_ldap_simple_bind_base_dn = '';
183+
SET GLOBAL authentication_ldap_simple_log_status = 1;
184+
SET GLOBAL authentication_ldap_simple_group_role_mapping = '';
185+
SET GLOBAL authentication_ldap_simple_group_search_filter = '(|(&(objectClass=posixGroup)(memberUid={UA}))(&(objectClass=group)(member={UD})))';
186+
187+
# -----------------------------------------------------------------
188+
# LDAP Cleanup
189+
# -----------------------------------------------------------------
190+
--perl EOF
191+
use strict;
192+
use warnings;
193+
194+
sub run_cmd {
195+
my @cmd = @_;
196+
my $pid = fork();
197+
die "fork: $!" unless defined $pid;
198+
if ($pid == 0) {
199+
open(STDOUT, '>', '/dev/null');
200+
open(STDERR, '>', '/dev/null');
201+
exec(@cmd) or exit(127);
202+
}
203+
waitpid($pid, 0);
204+
return $? >> 8;
205+
}
206+
207+
my $host = $ENV{'MTR_LDAP_HOST'} || 'localhost';
208+
my $port = $ENV{'MTR_LDAP_PORT'} || '389';
209+
my $ldap_uri = "ldap://$host:$port";
210+
my $admin_dn = 'cn=admin,dc=planetexpress,dc=com';
211+
my $admin_pw = 'GoodNewsEveryone';
212+
213+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
214+
"-w", $admin_pw,
215+
'cn=dn_test_group,ou=people,dc=planetexpress,dc=com');
216+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
217+
"-w", $admin_pw,
218+
'cn=Test\"User,ou=people,dc=planetexpress,dc=com');
219+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
220+
"-w", $admin_pw,
221+
'cn=Test(User),ou=people,dc=planetexpress,dc=com');
222+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
223+
"-w", $admin_pw,
224+
'cn=Test*User,ou=people,dc=planetexpress,dc=com');
225+
run_cmd("ldapdelete", "-x", "-H", $ldap_uri, "-D", $admin_dn,
226+
"-w", $admin_pw,
227+
'cn=Test\5cUser,ou=people,dc=planetexpress,dc=com');
228+
EOF
229+
230+
--source include/wait_until_count_sessions.inc
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dn: cn=Test\5cUser,ou=people,dc=planetexpress,dc=com
2+
objectClass: inetOrgPerson
3+
cn: Test\5cUser
4+
sn: User
5+
uid: testbackslashuser
6+
userPassword: testpass123
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dn: cn=Test(User),ou=people,dc=planetexpress,dc=com
2+
objectClass: inetOrgPerson
3+
cn: Test(User)
4+
sn: User
5+
uid: testparenuser
6+
userPassword: testpass123
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dn: cn=Test\"User,ou=people,dc=planetexpress,dc=com
2+
objectClass: inetOrgPerson
3+
cn: Test"User
4+
sn: User
5+
uid: testquoteuser
6+
userPassword: testpass123
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
dn: cn=dn_test_group,ou=people,dc=planetexpress,dc=com
2+
objectClass: groupOfNames
3+
cn: dn_test_group
4+
member: cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com
5+
member: cn=Test\"User,ou=people,dc=planetexpress,dc=com
6+
member: cn=Test(User),ou=people,dc=planetexpress,dc=com
7+
member: cn=Test*User,ou=people,dc=planetexpress,dc=com
8+
member: cn=Test\5cUser,ou=people,dc=planetexpress,dc=com

0 commit comments

Comments
 (0)