skip to content

In an RFC 4515 LDAP search filter, how is an asserted value containing a parenthesis, an asterisk or a backslash written?

level: middleimportance: must knowfreq 47%

answer

  1. the grammar's punctuation must be spelled out
  2. backslash plus two hexadecimal digits
  3. five reserved octets, not one
  4. one failure is loud, one is silent
  5. escape the value, never the assembled filter

basics

~20 s

RFC 4515 writes those octets as a backslash followed by two hexadecimal digits inside the asserted value: \28 for an open parenthesis, \29 for a close parenthesis, \2A for an asterisk, \5C for a backslash and \00 for NUL.

solid answer

~40 s

The string form of an LDAP search filter uses parentheses as its own delimiters, `&`, `|` and `!` as its combiners, and the asterisk as its substring wildcard. So an asserted value that itself contains one of those octets has to be escaped, and RFC 4515 escapes as a backslash plus two hexadecimal digits: `\28`, `\29`, `\2A`, `\5C` and `\00`. A clinician surnamed `Okonkwo (locum)` is asserted as `(sn=Okonkwo \28locum\29)`. The two failure shapes are different and the quiet one is worse: an unescaped parenthesis breaks the parse, so no filter is built at all, while an unescaped asterisk parses perfectly and turns an equality assertion into a substring assertion that matches a different set of entries.

code

pseudocode · 15 lines
pseudocode
function escape_assertion_value(value):
    out = empty string
    for each octet in value:
        if octet = "("   then append "\28" to out
        else if octet = ")"   then append "\29" to out
        else if octet = "*"   then append "\2A" to out
        else if octet = "\"   then append "\5C" to out
        else if octet = NUL   then append "\00" to out
        else append octet to out
    return out

// the parentheses and the '=' below are the filter's, not the value's
filter = "(&(objectClass=person)(sn="
       + escape_assertion_value(surname)
       + "))"

go deeper

for a junior

Recall that five octets are reserved by the filter's own grammar and that each is written as a backslash plus two hexadecimal digits inside an asserted value.

for a middle

Explain why an unescaped asterisk is the dangerous case: the filter still parses, silently becoming a substring assertion that matches a wider set of entries.

for a senior

Show that you escape values at the point they enter a filter, and that you know the filter string is parsed into a structured request before anything reaches the server.

for a principal

Consider where filter strings are authored across an estate - stored queries, configuration, operator tooling - and how you keep one preparation rule rather than several.

## The grammar's own punctuation RFC 4515 defines the **string representation** of an LDAP search filter. Its shape is small: a filter is a parenthesised `filtercomp`, and a `filtercomp` is an `and` introduced by `&`, an `or` introduced by `|`, a `not` introduced by `!`, or a single `item`. An item is an attribute description, a `filtertype` such as `=`, `~=`, `>=` or `<=`, and an asserted value. That leaves a handful of octets doing structural work: - `(` and `)` delimit every filter and every nested filter. - `*` is the wildcard: alone it is a **presence** item (`(sn=*)` means the entry has an `sn` attribute), and between fragments it forms a **substring** item with `initial`, `any` and `final` parts. - `\` introduces an escape, so it must be able to escape itself. - NUL cannot travel literally in the string form. An asserted value that contains one of those five has to say so explicitly, or the parser reads it as punctuation. ## The escape form | Octet | Written as | |---|---| | `(` | `\28` | | `)` | `\29` | | `*` | `\2A` | | `\` | `\5C` | | NUL | `\00` | The rule is general: a backslash followed by the octet's two hexadecimal digits. The hexadecimal digits are not case-sensitive, and the mechanism is not limited to those five - any octet may be written this way, which is how a value carrying arbitrary bytes, or a non-ASCII name whose UTF-8 encoding you would rather not paste literally, gets into a filter. Multi-byte characters are escaped **one octet at a time**, not one character at a time. ## The two failure shapes are not equally visible Take a teaching hospital whose clinician entries carry surnames typed by humans, including one recorded as `Okonkwo (locum)` and one recorded as `Ng*`. 1. **The parenthesis fails loudly.** `(sn=Okonkwo (locum))` has unbalanced structure: the parser sees a nested filter opening where an asserted value should continue. No valid filter is produced, so the search never runs. 2. **The asterisk fails quietly.** `(sn=Ng*)` is a perfectly legal filter - it is a substring item with `initial` set to `Ng`, and it matches every surname beginning with those two letters. Nothing errors. The answer is simply a different, larger set of entries than the one the caller meant, and the caller has no signal at all. Written correctly as `(sn=Ng\2A)`, it is an equality assertion against a surname whose last character is a literal asterisk. That asymmetry is the reason the escaping rule is worth knowing rather than delegating: the case that silently changes the meaning of your lookup is the case that does not complain. ## Escape the value, not the filter The escaping applies to the **asserted value** - and to an attribute description's options only where the grammar requires it - not to the assembled filter string. You escape each value as you place it into the filter, while the parentheses, the `&`, the `|`, the `!` and the `=` you wrote yourself stay as they are. Escaping the whole assembled string afterwards destroys the structure you just built. ## The string form is not the wire form One detail explains where this actually bites. The `SearchRequest` on the wire does not carry a filter string: it carries a structured `Filter`, encoded with the rest of the message. RFC 4515's grammar exists so that a filter can be written down - in a configuration file, a log line, a stored query, an interface that accepts one as text. The escaping therefore matters at the moment a string is **parsed into** that structure, which is why a malformed filter is usually rejected before any request reaches the directory server, and why the escaped form is a transport convention rather than something the directory stores. The corollary: the server compares the **unescaped** value under the attribute's matching rule. `(sn=Ng\2A)` matches an entry whose stored `sn` value is `Ng*`. Nothing anywhere holds `\2A`. ## A different set of rules lives next door A value placed inside a **Distinguished Name** follows the DN string syntax, which reserves a different set of characters and also permits a backslash directly before a character. The two escaping rules look alike and are not interchangeable, so a value bound for a search base and the same value bound for a filter are prepared differently.

  • What is the difference between the LDAP search filters (sn=*) and (sn=\2A)?
    `(sn=*)` is a **presence** item: it matches every entry that has an `sn` attribute at all, whatever its value. `(sn=\2A)` is an **equality** item whose asserted value is a single literal asterisk: it matches only an entry whose stored surname is the one-character string `*`. One asks whether the attribute exists, the other asks what it equals, and the escape is the whole difference.
  • What do the RFC 4526 filters (&) and (|) mean?
    They are the **absolute true** and **absolute false** filters. `(&)` is an `and` over an empty filter list, so it matches every entry in scope; `(|)` is an `or` over an empty list, so it matches none. `(&)` is the honest way to say "everything the scope selects" without asserting a presence item on some attribute that a particular entry may not carry.
  • Does the directory store or match on the escaped form of a value?
    No. The escape is a property of the filter's **string representation** only. Whatever parses the string turns `\2A` back into the octet before the structured filter is built, and the directory server compares the unescaped value under the attribute type's matching rule. An entry's stored value never contains the escape sequence, and searching for the literal text `\2A` would be a different assertion entirely.

saying these in an interview costs you the question

  • Says you strip parentheses and asterisks out of the value
  • Thinks an unescaped asterisk always makes the filter fail loudly
  • Escapes the assembled filter string, delimiters included
  • Believes the directory stores the escaped form of the value
  • Assumes DN escaping rules and filter escaping rules are the same
  • Thinks only the parentheses need escaping