14

I'm trying to add some users to my Ldap DB but I get some errors (invalid dn syntax) when I use some special characters like ",.". I need a function that escape all characters. I try preg_quote but I get some errors in some cases.

Thanks in advance

Code:

$user = 'Test , Name S.L';

    if(!(ldap_add($ds, "cn=" . $user . ",".LDAP_DN_BASE, $info))) {

            include 'error_new_account.php';

    }
Sbml
  • 1,907
  • 2
  • 16
  • 26

4 Answers4

29

EDIT Jan 2013: added support for escaping leading/trailing spaces in DN strings, per RFC 4514. Thanks to Eugenio for pointing out this issue.

EDIT 2014: I added this function to PHP 5.6. The code below is now a like-for-like drop-in replacement for earlier PHP versions.

if (!function_exists('ldap_escape')) {
    define('LDAP_ESCAPE_FILTER', 0x01);
    define('LDAP_ESCAPE_DN',     0x02);

    /**
     * @param string $subject The subject string
     * @param string $ignore Set of characters to leave untouched
     * @param int $flags Any combination of LDAP_ESCAPE_* flags to indicate the
     *                   set(s) of characters to escape.
     * @return string
     */
    function ldap_escape($subject, $ignore = '', $flags = 0)
    {
        static $charMaps = array(
            LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
            LDAP_ESCAPE_DN     => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
        );

        // Pre-process the char maps on first call
        if (!isset($charMaps[0])) {
            $charMaps[0] = array();
            for ($i = 0; $i < 256; $i++) {
                $charMaps[0][chr($i)] = sprintf('\\%02x', $i);;
            }

            for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) {
                $chr = $charMaps[LDAP_ESCAPE_FILTER][$i];
                unset($charMaps[LDAP_ESCAPE_FILTER][$i]);
                $charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
            }

            for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) {
                $chr = $charMaps[LDAP_ESCAPE_DN][$i];
                unset($charMaps[LDAP_ESCAPE_DN][$i]);
                $charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
            }
        }

        // Create the base char map to escape
        $flags = (int)$flags;
        $charMap = array();
        if ($flags & LDAP_ESCAPE_FILTER) {
            $charMap += $charMaps[LDAP_ESCAPE_FILTER];
        }
        if ($flags & LDAP_ESCAPE_DN) {
            $charMap += $charMaps[LDAP_ESCAPE_DN];
        }
        if (!$charMap) {
            $charMap = $charMaps[0];
        }

        // Remove any chars to ignore from the list
        $ignore = (string)$ignore;
        for ($i = 0, $l = strlen($ignore); $i < $l; $i++) {
            unset($charMap[$ignore[$i]]);
        }

        // Do the main replacement
        $result = strtr($subject, $charMap);

        // Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
        if ($flags & LDAP_ESCAPE_DN) {
            if ($result[0] === ' ') {
                $result = '\\20' . substr($result, 1);
            }
            if ($result[strlen($result) - 1] === ' ') {
                $result = substr($result, 0, -1) . '\\20';
            }
        }

        return $result;
    }
}

So you would do:

$user = 'Test , Name S.L';
$cn = ldap_escape($user, '', LDAP_ESCAPE_DN);
if (!ldap_add($ds, "cn={$cn}," . LDAP_DN_BASE, $info)) {
    include 'error_new_account.php';
}
Community
  • 1
  • 1
DaveRandom
  • 87,921
  • 11
  • 154
  • 174
  • Thank you very much, nice function. – Sbml Dec 19 '11 at 14:22
  • @daverandom you should add to the characters to escape also spaces (if they are first or last character of an attribute value), am I right? Also, the # sign should be escaped just if it is the first character of an attribute value. http://www-03.ibm.com/systems/i/software/ldap/underdn.html – Eugenio Jan 09 '13 at 17:10
  • @Eugenio Interesting, do you know if that is standard LDAP or IBM specific? I've never seen those stipulations anywhere before. The DAP/LDAP standards docs are very long and highly abstract, it's very hard to find little nuggets on information like that - and I'm having a hard time laying my hands on them again at the moment :S – DaveRandom Jan 09 '13 at 17:16
  • @DaveRandom should be standard LDAP, but for some reason all the escape functions I've seen googling today don't take into consideration that part. If you read here http://tools.ietf.org/html/rfc4514 it seems pretty evident that it's LDAP standard: [...] - a space (' ' U+0020) or number sign ('#' U+0023) occurring at the beginning of the string; - a space (' ' U+0020) character occurring at the end of the string; [...] I don't get the final sentence "Other characters may be escaped." though. – Eugenio Jan 09 '13 at 17:32
  • 1
    @Eugenio Indeed you are correct sir. The `#` will be taken care of because it is already in the DN escape list, the leading/trailing space issue will be taken care of by an edit in a couple of minutes. Looking at [RFC 4515](http://tools.ietf.org/html/rfc4515) the same rules do not apply to filers. I read the "Other characters may be escaped." sentence as "you can escape any character to it's \xx hex sequence, but only for the ones listed above is this required" – DaveRandom Jan 09 '13 at 18:03
  • @DaveRandom I think there are other things stipulated by [RFC 4514](https://tools.ietf.org/html/rfc4514) not covered in `LDAP_ESCAPE_DN`. Section 4 of the RFC covers encoding a carriage return. Not that I'd ever really want that encoded, it seems like it should be escaped with `\0d`. Unless I'm overlooking something? I just noticed this as I was creating a function to un-escape filter/DN values. Though carriage returns in DNs only seems valid with LDAPv3 and not LDAPv2. I think this is probably true of attribute values overall as well, though I'm unsure. – ChadSikorra Nov 07 '15 at 17:51
  • Ok, so after some testing, I can verify that it really is the case that a carriage return is allowed in a DN (at least in AD) and needs to be escaped with `\0d`. This seems to only apply to carriage returns, not line feeds. And it only needs to be escaped under the context of `LDAP_ESCAPE_DN`, as it doesn't apply to other attributes (the encoding seems to be handled automatically in that case). – ChadSikorra Nov 09 '15 at 01:43
  • Lifesaver! Working on php 5.5.x so was missing this function. Saved me a lot of time. – Pianoman Nov 15 '15 at 22:04
  • What is the use case for `LDAP_ESCAPE_DN`? In a DN, I would expect a comma to be escaped as `\,`. However, `ldap_escape` with `LDAP_ESCAPE_DN` escapes a comma as `\2c`. Why? – Gabriel Luci Feb 25 '22 at 16:34
3

PHP 5.6 Beta released ldap_escape() function recently and it is in effect, However, this version is not production ready at present, you can very use it for your development purposes as of now.

Shankar Narayana Damodaran
  • 68,075
  • 43
  • 96
  • 126
2

Just a heads up if your not on PHP 5.6 yet, you can mirror the exact PHP 5.6 function ldap_escape() using the methods I created below, keep in mind this is meant for use in a class. The above answer doesn't perform exactly like the ldap_escape function, as in it doesn't escape all characters into a hex string if no flags have been given, so this would be more suitable for a drop in replacement for earlier versions of PHP, in an object oriented way.

I've documented every line for an easier understanding on whats going on. Scroll down for output.

Methods (Compatible with PHP 5 or greater):

/**
 * Escapes the inserted value for LDAP.
 *
 * @param string $value The value to escape
 * @param string $ignore The characters to ignore
 * @param int $flags The PHP flag to use
 *
 * @return bool|string
 */
public function escapeManual($value, $ignore = '*', $flags = 0)
{
    /*
     * If a flag was supplied, we'll send the value
     * off to be escaped using the PHP flag values
     * and return the result.
     */
    if($flags) {
        return $this->escapeWithFlags($value, $ignore, $flags);
    }

    // Convert ignore string into an array
    $ignores = str_split($ignore);

    // Convert the value to a hex string
    $hex = bin2hex($value);

    /*
     * Separate the string, with the hex length of 2,
     * and place a backslash on the end of each section
     */
    $value = chunk_split($hex, 2, "\\");

    /*
     * We'll append a backslash at the front of the string
     * and remove the ending backslash of the string
     */
    $value = "\\" . substr($value, 0, -1);

    // Go through each character to ignore
    foreach($ignores as $charToIgnore)
    {
        // Convert the characterToIgnore to a hex
        $hexed = bin2hex($charToIgnore);

        // Replace the hexed variant with the original character
        $value = str_replace("\\" . $hexed, $charToIgnore, $value);
    }

    // Finally we can return the escaped value
    return $value;
}

/**
 * Escapes the inserted value with flags. Supplying either 1
 * or 2 into the flags parameter will escape only certain values
 *
 *
 * @param string $value The value to escape
 * @param string $ignore The characters to ignore
 * @param int $flags The PHP flag to use
 * @return bool|string
 */
public function escapeWithFlags($value, $ignore = '*', $flags = 0)
{
    // Convert ignore string into an array
    $ignores = str_split($ignore);

    $escapeFilter = ['\\', '*', '(', ')'];
    $escapeDn = ['\\', ',', '=', '+', '<', '>', ';', '"', '#'];

    switch($flags)
    {
        case 1:
            // Int 1 equals to LDAP_ESCAPE_FILTER
            $escapes = $escapeFilter;
            break;
        case 2:
            // Int 2 equals to LDAP_ESCAPE_DN
            $escapes = $escapeDn;
            break;
        case 3:
            // If both LDAP_ESCAPE_FILTER and LDAP_ESCAPE_DN are used
            $escapes = array_merge($escapeFilter, $escapeDn);
            break;
        default:
            // Customize your own default return value
            return false;
    }

    foreach($escapes as $escape)
    {
        // Make sure the escaped value isn't inside the ignore array
        if( ! in_array($escape, $ignores))
        {
            $hexed = chunk_split(bin2hex($escape), 2, "\\");

            $hexed = "\\" . substr($hexed, 0, -1);

            $value = str_replace($escape, $hexed, $value);
        }
    }

    return $value;
}

Tests (be aware that LDAP_ESCAPE constants are only available in PHP 5.6):

// Value to escape
$value = 'testing=+<>"";:#()*\x00';

$php = ldap_escape($value, $ignore = '*');

$man = $this->escapeManual($value, $ignore = '*');

echo $php; // \74\65\73\74\69\6e\67\3d\2b\3c\3e\22\22\3b\3a\23\28\29*\5c\78\30\30
echo $man; // \74\65\73\74\69\6e\67\3d\2b\3c\3e\22\22\3b\3a\23\28\29*\5c\78\30\30


$php = ldap_escape($value, $ignore = '*', LDAP_ESCAPE_DN);

$man = $this->escapeManual($value, $ignore = '*', LDAP_ESCAPE_DN);

echo $php; // testing\3d\2b\3c\3e\22\22\3b:\23()*\5cx00
echo $man; // testing\3d\2b\3c\3e\22\22\3b:\23()*\5cx00


$php = ldap_escape($value, $ignore = '*', LDAP_ESCAPE_FILTER);

$man = $this->escapeManual($value, $ignore = '*', LDAP_ESCAPE_FILTER);

echo $php; // testing=+<>"";:#\28\29*\5cx00
echo $man; // testing=+<>"";:#\28\29*\5cx00

Github Gist link: https://gist.github.com/stevebauman/0db9b5daa414d60fc266

Steve Bauman
  • 8,165
  • 7
  • 40
  • 56
1

Those characters must escaped to be part of the data of a distinguished name or relative distinguished name. Escape the character (as in all LDAP) with a backslash 2 hex digit, such as \2a. Anything else would not be in compliance with the standards body documents. See RFC4514 for more specific information regarding the string representation of distinguished names.

Community
  • 1
  • 1
Terry Gardner
  • 10,957
  • 2
  • 28
  • 38