How LDAP Injection Happens in C with OpenLDAP and How to Fix It
Summary
A high-severity LDAP injection vulnerability was discovered in the OpenSIPS H350 module, where the ldap_rfc4515_escape() function failed to escape the NUL byte (\0) — one of the special characters defined in RFC 4515. This gap meant that crafted SIP URI values could bypass the escaping logic and manipulate LDAP filter queries. The fix adds explicit NUL byte escaping and replaces potentially unsafe strncpy calls with memcpy to ensure correct buffer handling.
Introduction
The src/modules/h350/h350_exp_fn.c file in OpenSIPS handles H.350 directory lookups — it takes SIP URI values from incoming SIP messages and uses them to query an LDAP directory. The lookup is constructed using the H350_SIPURI_LOOKUP_LDAP_FILTER macro, which embeds a SIP URI into an LDAP filter string.
A variable named sip_uri_escaped hints that escaping was intended. And indeed, ldap_rfc4515_escape() in src/modules/ldap/ldap_escape.c does escape several dangerous characters. But it missed one: the NUL byte (\0).
That single omission is enough to make the escaping incomplete — and LDAP injection possible through a crafted SIP header.
The Vulnerability Explained
What RFC 4515 Requires
RFC 4515 defines the string representation of LDAP search filters. It specifies that the following characters must be escaped when they appear in filter values:
| Character | Escaped Form |
|---|---|
* |
\2a |
( |
\28 |
) |
\29 |
\ |
\5c |
NUL (\0) |
\00 |
Before this fix, the ldap_rfc4515_escape() function handled *, (, ), and \ — but not the NUL byte. Here's the relevant section of the vulnerable code:
while(src < (sin->s + sin->len)) {
switch(*src) {
case '*':
*dst++ = '\\';
*dst++ = '2';
*dst = 'a';
break;
// ... other cases for (, ), \
// ❌ No case for '\0'
}
}
The absence of a case '\0': branch means that if a NUL byte appears in the input, it passes through unescaped.
Why NUL Bytes Are Dangerous in LDAP Filters
In C, strings are NUL-terminated. If an attacker can inject a NUL byte into a SIP URI value that gets embedded into an LDAP filter, the behavior depends on the LDAP library:
- Some LDAP libraries treat the filter string as a C string and stop reading at the NUL, effectively truncating the filter. This can remove closing parentheses or additional constraints, producing a malformed or overly permissive filter.
- Others may process the full binary buffer, where the NUL byte can act as a wildcard or separator depending on the LDAP server implementation.
Attack Scenario
Consider a SIP From: header crafted like this:
From: <sip:admin\x00*@example.com>
When this URI is passed through an incomplete escaping function and embedded into an LDAP filter such as:
(SIPIdentityUserName=admin\x00*)
A C-string-based LDAP library might truncate the filter at the NUL byte, resulting in:
(SIPIdentityUserName=admin
This is a malformed filter that could cause unexpected behavior — returning unintended results, bypassing authentication checks, or crashing the LDAP query processor. In a VoIP environment where H.350 is used to authenticate SIP users against an LDAP directory, this could allow an attacker to impersonate users or bypass credential verification entirely.
The Fix
The fix touches two files, each addressing a distinct problem.
1. src/modules/ldap/ldap_escape.c — Adding NUL Byte Escaping
The core fix adds a case '\0': branch to the existing switch statement in ldap_rfc4515_escape():
Before:
while(src < (sin->s + sin->len)) {
switch(*src) {
case '*':
*dst++ = '\\';
*dst++ = '2';
*dst = 'a';
break;
// ... handles (, ), \ but NOT \0
}
}
After:
while(src < (sin->s + sin->len)) {
switch(*src) {
case '\0': // ✅ NEW: NUL byte now escaped
*dst++ = '\\';
*dst++ = '0';
*dst = '0';
break;
case '*':
*dst++ = '\\';
*dst++ = '2';
*dst = 'a';
break;
// ... rest of cases
}
}
The NUL byte is now encoded as \00 per RFC 4515, preventing it from being passed raw into the LDAP filter string.
2. src/modules/h350/h350_exp_fn.c — Replacing strncpy with memcpy
The second change replaces two strncpy calls with memcpy in the h350_auth_lookup() function:
Before (line 138 and 155):
strncpy(username_avp_name_buf, username_avp_name.s.s,
username_avp_name.s.len);
// ...
strncpy(password_avp_name_buf, password_avp_name.s.s,
password_avp_name.s.len);
After:
memcpy(username_avp_name_buf, username_avp_name.s.s,
username_avp_name.s.len);
// ...
memcpy(password_avp_name_buf, password_avp_name.s.s,
password_avp_name.s.len);
This change matters because strncpy has subtle, often-misunderstood behavior: when the source string is shorter than n, it pads the remainder with NUL bytes. When the source is longer, it does not NUL-terminate the destination. Since the code already manually appends \0 at username_avp_name_buf[username_avp_name.s.len] = '\0', using memcpy with the exact length is both more explicit and more correct. It copies exactly s.len bytes without the ambiguous padding behavior of strncpy.
Together, these two changes close the injection vector and improve the correctness of buffer handling in the authentication lookup path.
Prevention & Best Practices
1. Implement Full RFC 4515 Compliance from the Start
When writing LDAP escaping functions in C, don't cherry-pick special characters. Implement the complete set defined in RFC 4515 from day one:
// All RFC 4515 special characters must be escaped
// *, (, ), \, NUL (and / in DNs per RFC 4514)
Write a test case for each one. A missing case in a switch statement is a classic C bug that's easy to introduce and hard to spot in code review.
2. Use memcpy for Length-Bounded Copies When You Control NUL Termination
The strncpy function is widely misunderstood. In OpenSIPS-style code where string lengths are tracked explicitly (via str structs with .s and .len), memcpy + manual NUL termination is cleaner and less error-prone:
memcpy(buf, src.s, src.len);
buf[src.len] = '\0';
This pattern is explicit about what it does and avoids strncpy's padding behavior.
3. Validate at the Boundary, Escape at the Sink
SIP URI values from incoming messages should be treated as untrusted input. Apply escaping immediately before they are used in LDAP filters — not earlier, not later. This keeps the escaping logic close to the dangerous operation.
4. Reference Standards Explicitly in Code Comments
/* Escape per RFC 4515 Section 3: *, (, ), \, NUL */
A comment like this makes it immediately obvious to the next developer that all six characters must be handled, reducing the chance of a future regression.
5. Use Static Analysis
Tools like Semgrep can be configured to trace tainted data from SIP message parsing functions to LDAP filter construction calls, flagging cases where the escaping function is missing or incomplete.
Relevant standards:
- OWASP LDAP Injection Prevention Cheat Sheet
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query
Key Takeaways
- Incomplete escaping is still vulnerable escaping. The
ldap_rfc4515_escape()function handled most RFC 4515 special characters but missed NUL — one missingcasein a switch statement was enough to leave an injection vector open. - Always implement the full RFC 4515 character set. LDAP filter escaping must cover all six defined special characters:
*,(,),\, NUL, and/. Partial compliance creates a false sense of security. - NUL bytes are a real attack vector in C LDAP code. Because C strings are NUL-terminated, an unescaped NUL byte in a filter value can truncate or corrupt the filter in ways that are hard to predict and easy to exploit.
- Prefer
memcpyoverstrncpywhen you control NUL termination. Inh350_auth_lookup(), the code already manually NUL-terminates the buffer —strncpy's implicit behavior was redundant and potentially misleading. - SIP URI values are attacker-controlled input. Any code path that takes data from SIP headers and uses it in a system query (LDAP, SQL, shell) must treat that data as untrusted and apply appropriate escaping.
How Orbis AppSec Detected This
- Source: SIP URI values extracted from incoming SIP message headers (e.g.,
From:,To:) inh350_exp_fn.c - Sink: LDAP filter string construction using the
H350_SIPURI_LOOKUP_LDAP_FILTERmacro insrc/modules/h350/h350_exp_fn.c:38, passing throughldap_rfc4515_escape()insrc/modules/ldap/ldap_escape.c - Missing control: The
ldap_rfc4515_escape()function did not include acase '\0':branch, leaving the NUL byte unescaped despite RFC 4515 requiring it - CWE: CWE-90: Improper Neutralization of Special Elements used in an LDAP Query
- Fix: Added
case '\0':to the escaping switch inldap_escape.cto encode NUL as\00, and replacedstrncpywithmemcpyinh350_exp_fn.cfor correct length-bounded buffer copying
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability is a textbook example of how partial compliance with a security standard can be just as dangerous as no compliance at all. The ldap_rfc4515_escape() function was doing the right thing — but it was doing it incompletely. A single missing case '\0': in a switch statement left a gap that an attacker could exploit by embedding a NUL byte in a SIP URI to manipulate LDAP filter logic in the H.350 authentication path.
The fix is small — just a few lines — but the impact is significant. It brings the escaping function into full RFC 4515 compliance and removes a subtle but real injection vector from a security-critical code path. The accompanying strncpy → memcpy change is a good example of defensive cleanup: removing ambiguous behavior even when it isn't directly exploitable.
For developers working on SIP, LDAP, or any code that bridges user-controlled input into directory queries: always implement the full escaping specification, test each special character explicitly, and treat every field from a network message as untrusted input.
References
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query
- OWASP LDAP Injection Prevention Cheat Sheet
- RFC 4515: Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters
- Semgrep rules for LDAP injection
- fix: the h350 module constructs ldap filters using s... in h350_exp_fn.c