test/ldap: add LDAP filter-injection reproducers

Add tests that reproduce LDAP filter injection via unescaped {USER}
substitution (SCYLLADB-1309).  A wildcard username ('*') matches
every group entry, and a parenthesis payload (")(uid=*") breaks the
search filter.

Extend the LDAP test fixture (ldap_server.py, slapd.conf) with
memberUid attributes and the NIS schema so the new tests can
exercise direct filter-value substitution.
This commit is contained in:
Piotr Smaron
2026-04-01 15:13:45 +02:00
parent a0e79f391f
commit ecc3bcabd4
3 changed files with 49 additions and 0 deletions

View File

@@ -277,6 +277,10 @@ const auto flaky_server_query_template = fmt::format(
"ldap://localhost:{}/{}?cn?sub?(uniqueMember=uid={{USER}},ou=People,dc=example,dc=com)",
std::stoi(ldap_port) + 2, base_dn);
const auto member_uid_query_template = fmt::format(
"ldap://localhost:{}/dc=example,dc=com?cn?sub?(memberUid={{USER}})",
ldap_port);
auto make_ldap_manager(cql_test_env& env, sstring query_template = default_query_template) {
auto stop_role_manager = [] (auth::ldap_role_manager* m) {
m->stop().get();
@@ -340,6 +344,42 @@ SEASTAR_TEST_CASE(ldap_wrong_role) {
});
}
SEASTAR_TEST_CASE(ldap_filter_injection_with_wildcard_user) {
return do_with_cql_env_thread([](cql_test_env& env) {
auto m = make_ldap_manager(env, member_uid_query_template);
m->start().get();
do_with_mc(env, [&] (::service::group0_batch& b) {
m->create("*", auth::role_config{.is_superuser = false, .can_login = true}, b).get();
m->create("role1", auth::role_config{}, b).get();
m->create("role2", auth::role_config{.is_superuser = true, .can_login = false}, b).get();
});
// BUG: Without escaping, '*' is interpolated literally into the LDAP
// filter (memberUid=*), which matches all group entries.
const role_set expected{"*", "role1", "role2"};
BOOST_REQUIRE_EQUAL(expected, m->query_granted("*", auth::recursive_role_query::no).get());
});
}
SEASTAR_TEST_CASE(ldap_filter_injection_with_parenthesis_payload) {
return do_with_cql_env_thread([](cql_test_env& env) {
auto m = make_ldap_manager(env, member_uid_query_template);
m->start().get();
// BUG: Without escaping, the payload ")(uid=*" produces the malformed
// filter (memberUid=)(uid=*), which the LDAP server rejects outright.
const sstring payload = ")(uid=*";
do_with_mc(env, [&] (::service::group0_batch& b) {
m->create(payload, auth::role_config{.is_superuser = false, .can_login = true}, b).get();
m->create("role1", auth::role_config{}, b).get();
m->create("role2", auth::role_config{.is_superuser = true, .can_login = false}, b).get();
});
BOOST_REQUIRE_EXCEPTION(m->query_granted(payload, auth::recursive_role_query::no).get(),
std::runtime_error, exception_predicate::message_contains("Bad search filter"));
});
}
SEASTAR_TEST_CASE(ldap_reconnect) {
return do_with_cql_env_thread([](cql_test_env& env) {
auto m = make_ldap_manager(env, flaky_server_query_template);

View File

@@ -60,17 +60,24 @@ userid: jdoe
userPassword: pa55w0rd
""", """dn: cn=role1,dc=example,dc=com
objectClass: groupOfUniqueNames
objectClass: extensibleObject
cn: role1
uniqueMember: uid=jsmith,ou=People,dc=example,dc=com
uniqueMember: uid=cassandra,ou=People,dc=example,dc=com
memberUid: jsmith
memberUid: cassandra
""", """dn: cn=role2,dc=example,dc=com
objectClass: groupOfUniqueNames
objectClass: extensibleObject
cn: role2
uniqueMember: uid=cassandra,ou=People,dc=example,dc=com
memberUid: cassandra
""", """dn: cn=role3,dc=example,dc=com
objectClass: groupOfUniqueNames
objectClass: extensibleObject
cn: role3
uniqueMember: uid=jdoe,ou=People,dc=example,dc=com
memberUid: jdoe
""", ]

View File

@@ -8,6 +8,8 @@ rootdn "cn=admin,cn=config"
pidfile ./pidfile.pid
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/nis.schema
database mdb
suffix "dc=example,dc=com"