Ldap Injection in Actix
How Ldap Injection Manifests in Actix
Ldap Injection in Actix applications typically occurs when user input is directly incorporated into LDAP queries without proper sanitization. Actix developers often use LDAP for authentication backends, directory services, or user management systems, making this vulnerability particularly relevant.
use actix_web::{web, App, HttpServer, Responder};
use ldap3::{Ldap, Scope, SearchOptions};
async fn search_users(query: web::Query<UserQuery>) -> impl Responder {
let ldap = Ldap::new("ldap://localhost:389").unwrap();
let base_dn = "ou=users,dc=example,dc=com";
// Vulnerable: direct interpolation of user input
let filter = format!("(&(objectClass=person)(cn={}))", query.name);
let (result, _err) = ldap.search(
base_dn,
Scope::Sub,
&filter,
vec!["cn", "mail"]
).await.unwrap();
serde_json::to_string(&result).unwrap()
}
struct UserQuery { name: String }The vulnerability in this Actix handler is clear: the query.name parameter is directly interpolated into the LDAP filter string. An attacker could submit a payload like *)(objectClass=*))(|(objectClass=* to bypass authentication or extract sensitive directory information. The lack of parameterized queries means LDAP injection is possible.
Another common pattern in Actix applications involves using LDAP for authentication with Active Directory or OpenLDAP backends:
async fn authenticate(credentials: web::Json<Credentials>) -> impl Responder {
let ldap = Ldap::new("ldap://ad.example.com").unwrap();
let user_dn = format!("cn={},ou=users,dc=example,dc=com", credentials.username);
// Vulnerable: username directly interpolated into DN
match ldap.simple_bind(&user_dn, &credentials.password).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::Unauthorized().finish()
}
}
struct Credentials { username: String, password: String }Here, an attacker could craft a username like admin),dc=example,dc=com
(|(cn=* to manipulate the bind DN and potentially authenticate as a different user without knowing their password.
Actix-Specific Detection
Detecting LDAP injection in Actix applications requires examining both the code patterns and runtime behavior. middleBrick's black-box scanning approach is particularly effective for this, as it can test the unauthenticated attack surface without requiring source code access.
When scanning an Actix API endpoint that might be vulnerable to LDAP injection, middleBrick tests for several attack patterns:
POST /api/search HTTP/1.1
Host: example.com
Content-Type: application/json
{
"name": "*)(objectClass=*))(|(objectClass=*"
}
POST /api/authenticate HTTP/1.1
Host: example.com
Content-Type: application/json
{
"username": "admin),dc=example,dc=com\n(|(cn=*",
"password": "irrelevant"
}
POST /api/search HTTP/1.1
Host: example.com
Content-Type: application/json
{
"name": "*")(&(objectClass=person)(mail=*)(cn=*")"
}These payloads test for common LDAP injection patterns including filter manipulation, DN manipulation, and attribute injection. middleBrick's 12 security checks include specific LDAP injection detection that analyzes responses for signs of successful injection, such as unexpected results, error messages revealing directory structure, or authentication bypass.
For Actix developers, the CLI tool provides a quick way to scan your own endpoints:
npx middlebrick scan https://api.example.com/search?name=test
# Or scan with the npm package installed
middlebrick scan --url https://api.example.com/authenticate --method POST --data '{"username":"test","password":"test"}'The scanner will report findings with severity levels and provide remediation guidance specific to LDAP injection patterns found in your Actix application.
Actix-Specific Remediation
Remediating LDAP injection in Actix applications requires using parameterized queries or proper input sanitization. The ldap3 crate provides mechanisms to safely construct LDAP queries without string interpolation.
Here's a secure version of the search endpoint:
use actix_web::{web, App, HttpServer, Responder};
use ldap3::{Ldap, Scope, SearchOptions, Escape};
async fn search_users(query: web::Query<UserQuery>) -> impl Responder {
let ldap = Ldap::new("ldap://localhost:389").unwrap();
let base_dn = "ou=users,dc=example,dc=com";
// Secure: use parameterized query with proper escaping
let filter = format!("(&(objectClass=person)(cn={}))", Escape::ldap(&query.name));
let (result, _err) = ldap.search(
base_dn,
Scope::Sub,
&filter,
vec!["cn", "mail"]
).await.unwrap();
serde_json::to_string(&result).unwrap()
}
struct UserQuery { name: String }The key change is using Escape::ldap() to properly escape special LDAP characters. This prevents injection by ensuring user input cannot break out of the intended query structure.
For authentication endpoints, use the built-in bind mechanism with proper DN construction:
async fn authenticate(credentials: web::Json<Credentials>) -> impl Responder {
let ldap = Ldap::new("ldap://ad.example.com").unwrap();
// Secure: use parameterized bind with escaped DN
let user_dn = format!("cn={},ou=users,dc=example,dc=com", Escape::ldap(&credentials.username));
match ldap.simple_bind(&user_dn, &credentials.password).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::Unauthorized().finish()
}
}
struct Credentials { username: String, password: String }Alternatively, use a more robust authentication approach with proper error handling:
async fn authenticate(credentials: web::Json<Credentials>) -> impl Responder {
let ldap = Ldap::new("ldap://ad.example.com").unwrap();
// Use a safer approach with proper DN escaping
let escaped_user = Escape::ldap(&credentials.username);
let user_dn = format!("cn={},ou=users,dc=example,dc=com", escaped_user);
// Check if the DN is valid before attempting bind
if !is_valid_dn(&user_dn) {
return HttpResponse::BadRequest().body("Invalid username format");
}
match ldap.simple_bind(&user_dn, &credentials.password).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::Unauthorized().finish()
}
}
fn is_valid_dn(dn: &str) -> bool {
// Simple validation to prevent DN injection
!dn.contains("\n") && !dn.contains("\r") && dn.len() < 255
}For production Actix applications, consider using middleware to validate and sanitize LDAP inputs across all endpoints:
use actix_web::{dev::Payload, web::Data, FromRequest, HttpRequest};
struct LdapSafeString(String);
#[async_trait::async_trait]
impl FromRequest for LdapSafeString {
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
async fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future::Output {
// Extract and sanitize input
let input = req.match_info().get("param").unwrap_or("").to_string();
let safe = Escape::ldap(&input);
ready(Ok(LdapSafeString(safe)))
}
}
async fn secure_search(ldap_safe: LdapSafeString) -> impl Responder {
// ldap_safe.0 is now safe to use in LDAP queries
HttpResponse::Ok().body(format!("Searching for: {}", ldap_safe.0))
}