From d5086d85d3290fd9dd1747db88faeae00bf029e8 Mon Sep 17 00:00:00 2001 From: Jeffrey Walton Date: Sun, 18 Jun 2023 20:30:56 -0400 Subject: [PATCH 1/2] Update LDAP encoders This commit updates: * LDAP encoder documentation; * LDAP encoding in encodeForDN and encodeForLDAP; * LDAP tests for encodeForDN and encodeForLDAP --- src/main/java/org/owasp/esapi/Encoder.java | 41 +++++++++ .../owasp/esapi/reference/DefaultEncoder.java | 30 ++++++- .../owasp/esapi/reference/EncoderTest.java | 85 ++++++++++++++++--- 3 files changed, 144 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/owasp/esapi/Encoder.java b/src/main/java/org/owasp/esapi/Encoder.java index 4cae3f29a..22ae8f94b 100644 --- a/src/main/java/org/owasp/esapi/Encoder.java +++ b/src/main/java/org/owasp/esapi/Encoder.java @@ -444,32 +444,73 @@ public interface Encoder { /** * Encode data for use in LDAP queries. Wildcard (*) characters will be encoded. * + * This encoder operates according to RFC 4515, Section 3. RFC 4515 says the following character ranges + * are valid: 0x01-0x27, 0x2B-0x5B and 0x5D-0x7F. Characters outside the ranges are hex encoded, and they + * include 0x00 (NUL), 0x28 (LPAREN), 0x29 (RPAREN), 0x2A (ASTERISK), and 0x5C (ESC). The encoder will also + * encode 0x2F (FSLASH), which is required by Microsoft Active Directory. + * + * NB: At ESAPI 2.5.3, {@code encodeForLDAP} began strict conformance with RFC 4515. Characters above 0x7F + * are converted to UTF-8, and then the byte sequences are hex encoded according to the RFC. + * * @param input * the text to encode for LDAP * * @return input encoded for use in LDAP + * + * @see RFC 4515, Lightweight Directory Access Protocol + * (LDAP): String Representation of Search Filters + * + * @since ESAPI 1.3 */ String encodeForLDAP(String input); /** * Encode data for use in LDAP queries. You have the option whether or not to encode wildcard (*) characters. * + * This encoder operates according to RFC 4515, Section 3. RFC 4515 says the following character ranges + * are valid: 0x01-0x27, 0x2B-0x5B and 0x5D-0x7F. Characters outside the ranges are hex encoded, and they + * include 0x00 (NUL), 0x28 (LPAREN), 0x29 (RPAREN), 0x2A (ASTERISK), and 0x5C (ESC). The encoder will also + * encode 0x2F (FSLASH), which is required by Microsoft Active Directory. + * + * NB: At ESAPI 2.5.3, {@code encodeForLDAP} began strict conformance with RFC 4515. Characters above 0x7F + * are converted to UTF-8, and then the byte sequences are hex encoded according to the RFC. + * * @param input * the text to encode for LDAP * @param encodeWildcards * whether or not wildcard (*) characters will be encoded. * * @return input encoded for use in LDAP + * + * @see RFC 4515, Lightweight Directory Access Protocol + * (LDAP): String Representation of Search Filters + * + * @since ESAPI 1.3 */ String encodeForLDAP(String input, boolean encodeWildcards); /** * Encode data for use in an LDAP distinguished name. * + * This encoder operates according to RFC 4514, Section 3. RFC 4514 says the following character ranges + * are valid: 0x01-0x21, 0x23-0x2A, 0x2D-0x3A, 0x3D, 0x3F-0x5B, 0x5D-0x7F. Characters outside the ranges + * are hex encoded, and they include 0x00 (NUL), 0x22 (DQUOTE), 0x2B (PLUS), 0x2C (COMMA), + * 0x3B (SEMI), 0x3C (LANGLE), 0x3E (RANGLE) and 0x5C (ESC). The encoder will also encode 0x2F (FSLASH), + * which is required by Microsoft Active Directory. The leading and trailing characters in a distinguished + * name string will also have 0x20 (SPACE) and 0x23 (SHARP) encoded. + * + * NB: At ESAPI 2.5.3, {@code encodeForDN} began strict conformance with RFC 4514. Characters above 0x7F + * are converted to UTF-8, and then the byte sequences are hex encoded according to the RFC. + * * @param input * the text to encode for an LDAP distinguished name * * @return input encoded for use in an LDAP distinguished name + * + * @see RFC 4514, Lightweight Directory Access Protocol + * (LDAP): String Representation of Distinguished Names + * + * @since ESAPI 1.3 */ String encodeForDN(String input); diff --git a/src/main/java/org/owasp/esapi/reference/DefaultEncoder.java b/src/main/java/org/owasp/esapi/reference/DefaultEncoder.java index 35d8d29e2..a754064bc 100644 --- a/src/main/java/org/owasp/esapi/reference/DefaultEncoder.java +++ b/src/main/java/org/owasp/esapi/reference/DefaultEncoder.java @@ -311,6 +311,7 @@ public String encodeForLDAP(String input, boolean encodeWildcards) { // According to Microsoft docs [1,2], the forward slash ('/') MUST be escaped. // According to RFC 4515 Section 3 [3], the forward slash (and other characters) MAY be escaped. // Since Microsoft is a MUST, escape forward slash for all implementations. Also see discussion at [4]. + // Characters above 0x7F are converted to UTF-8 and then hex encoded in the default case. // [1] https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax // [2] https://social.technet.microsoft.com/wiki/contents/articles/5312.active-directory-characters-to-escape.aspx // [3] https://tools.ietf.org/search/rfc4515#section-3 @@ -343,7 +344,18 @@ public String encodeForLDAP(String input, boolean encodeWildcards) { sb.append("\\00"); break; default: - sb.append(c); + if (c >= 0x80) { + try { + final byte[] u = String.valueOf(c).getBytes("UTF-8"); + for (byte b : u) { + sb.append(String.format("\\%02x", b)); + } + } catch (UnsupportedEncodingException ex) { + // UTF-8 is always supported + } + } else { + sb.append(c); + } } } return sb.toString(); @@ -365,6 +377,9 @@ public String encodeForDN(String input) { for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); switch (c) { + case '\0': + sb.append("\\00"); + break; case '\\': sb.append("\\\\"); break; @@ -390,7 +405,18 @@ public String encodeForDN(String input) { sb.append("\\;"); break; default: - sb.append(c); + if (c >= 0x80) { + try { + final byte[] u = String.valueOf(c).getBytes("UTF-8"); + for (byte b : u) { + sb.append(String.format("\\%02x", b)); + } + } catch (UnsupportedEncodingException ex) { + // UTF-8 is always supported + } + } else { + sb.append(c); + } } } // add the trailing backslash if needed diff --git a/src/test/java/org/owasp/esapi/reference/EncoderTest.java b/src/test/java/org/owasp/esapi/reference/EncoderTest.java index 87a9b6c33..101840992 100644 --- a/src/test/java/org/owasp/esapi/reference/EncoderTest.java +++ b/src/test/java/org/owasp/esapi/reference/EncoderTest.java @@ -528,46 +528,111 @@ public void testMySQLANSIModeQuoteInjection() { /** * Test of encodeForLDAP method, of class org.owasp.esapi.Encoder. + * + * Additional tests: https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt */ public void testEncodeForLDAP() { System.out.println("encodeForLDAP"); Encoder instance = ESAPI.encoder(); assertEquals(null, instance.encodeForLDAP(null)); - assertEquals("No special characters to escape", "Hi This is a test #��", instance.encodeForLDAP("Hi This is a test #��")); - assertEquals("Zeros", "Hi \\00", instance.encodeForLDAP("Hi \u0000")); - assertEquals("LDAP Christams Tree", "Hi \\28This\\29 = is \\2a a \\5c test # � � �", instance.encodeForLDAP("Hi (This) = is * a \\ test # � � �")); + assertEquals("No special characters to escape", "Hi This is a test", instance.encodeForLDAP("Hi This is a test")); + assertEquals("No special characters to escape", "Hi This is a test \u0007f", instance.encodeForLDAP("Hi This is a test \u0007f")); + assertEquals("Special characters to escape", "Hi This is a test \\c2\\80", instance.encodeForLDAP("Hi This is a test \u0080")); + assertEquals("Special characters to escape", "Hi This is a test \\c3\\bf", instance.encodeForLDAP("Hi This is a test \u00FF")); + assertEquals("Special characters to escape", "Hi This is a test \\df\\bf", instance.encodeForLDAP("Hi This is a test \u07FF")); + assertEquals("Special characters to escape", "Hi This is a test \\e0\\a0\\80", instance.encodeForLDAP("Hi This is a test \u0800")); + assertEquals("Special characters to escape", "Hi This is a test \\e0\\a3\\bf", instance.encodeForLDAP("Hi This is a test \u08FF")); + assertEquals("Special characters to escape", "Hi This is a test \\e7\\bf\\bf", instance.encodeForLDAP("Hi This is a test \u7FFF")); + assertEquals("Special characters to escape", "Hi This is a test \\e8\\80\\80", instance.encodeForLDAP("Hi This is a test \u8000")); + assertEquals("Special characters to escape", "Hi This is a test \\e8\\bf\\bf", instance.encodeForLDAP("Hi This is a test \u8FFF")); + assertEquals("Special characters to escape", "Hi This is a test \\ef\\bf\\bf", instance.encodeForLDAP("Hi This is a test \uFFFF")); + assertEquals("Special characters to escape", "Hi This is a test #\\ef\\bf\\bd\\ef\\bf\\bd", instance.encodeForLDAP("Hi This is a test #��")); + assertEquals("NUL", "Hi \\00", instance.encodeForLDAP("Hi \u0000")); + assertEquals("LPAREN", "Hi \\28", instance.encodeForLDAP("Hi (")); + assertEquals("RPAREN", "Hi \\29", instance.encodeForLDAP("Hi )")); + assertEquals("ASTERISK", "Hi \\2a", instance.encodeForLDAP("Hi *")); + assertEquals("SLASH", "Hi \\2f", instance.encodeForLDAP("Hi /")); + assertEquals("ESC", "Hi \\5c", instance.encodeForLDAP("Hi \\")); + assertEquals("LDAP Christams Tree", "Hi \\28This\\29 = is \\2a a \\5c test # \\ef\\bf\\bd \\ef\\bf\\bd \\ef\\bf\\bd", instance.encodeForLDAP("Hi (This) = is * a \\ test # � � �")); assertEquals("Hi \\28This\\29 =", instance.encodeForLDAP("Hi (This) =")); assertEquals("Forward slash for \\2fMicrosoft\\2f \\2fAD\\2f", instance.encodeForLDAP("Forward slash for /Microsoft/ /AD/")); + assertEquals("RFC 4515, Section 4", "(cn=Babs Jensen)", "(cn=" + instance.encodeForLDAP("Babs Jensen") + ")"); } /** * Test of encodeForLDAP method with without encoding wildcard characters, of class org.owasp.esapi.Encoder. + * + * Additional tests: https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt */ public void testEncodeForLDAPWithoutEncodingWildcards() { System.out.println("encodeForLDAPWithoutEncodingWildcards"); Encoder instance = ESAPI.encoder(); assertEquals(null, instance.encodeForLDAP(null, false)); - assertEquals("No special characters to escape", "Hi This is a test #��", instance.encodeForLDAP("Hi This is a test #��", false)); - assertEquals("Zeros", "Hi \\00", instance.encodeForLDAP("Hi \u0000", false)); - assertEquals("LDAP Christams Tree", "Hi \\28This\\29 = is * a \\5c test # � � �", instance.encodeForLDAP("Hi (This) = is * a \\ test # � � �", false)); + assertEquals("No special characters to escape", "Hi This is a test", instance.encodeForLDAP("Hi This is a test")); + assertEquals("No special characters to escape", "Hi This is a test \u0007f", instance.encodeForLDAP("Hi This is a test \u0007f", false)); + assertEquals("Special characters to escape", "Hi This is a test \\c2\\80", instance.encodeForLDAP("Hi This is a test \u0080", false)); + assertEquals("Special characters to escape", "Hi This is a test \\c3\\bf", instance.encodeForLDAP("Hi This is a test \u00FF", false)); + assertEquals("Special characters to escape", "Hi This is a test \\df\\bf", instance.encodeForLDAP("Hi This is a test \u07FF", false)); + assertEquals("Special characters to escape", "Hi This is a test \\e0\\a0\\80", instance.encodeForLDAP("Hi This is a test \u0800", false)); + assertEquals("Special characters to escape", "Hi This is a test \\e0\\a3\\bf", instance.encodeForLDAP("Hi This is a test \u08FF", false)); + assertEquals("Special characters to escape", "Hi This is a test \\e7\\bf\\bf", instance.encodeForLDAP("Hi This is a test \u7FFF", false)); + assertEquals("Special characters to escape", "Hi This is a test \\e8\\80\\80", instance.encodeForLDAP("Hi This is a test \u8000", false)); + assertEquals("Special characters to escape", "Hi This is a test \\e8\\bf\\bf", instance.encodeForLDAP("Hi This is a test \u8FFF", false)); + assertEquals("Special characters to escape", "Hi This is a test \\ef\\bf\\bf", instance.encodeForLDAP("Hi This is a test \uFFFF", false)); + assertEquals("Special characters to escape", "Hi This is a test #\\ef\\bf\\bd\\ef\\bf\\bd", instance.encodeForLDAP("Hi This is a test #��", false)); + assertEquals("NUL", "Hi \\00", instance.encodeForLDAP("Hi \u0000", false)); + assertEquals("LPAREN", "Hi \\28", instance.encodeForLDAP("Hi (", false)); + assertEquals("RPAREN", "Hi \\29", instance.encodeForLDAP("Hi )", false)); + assertEquals("ASTERISK", "Hi *", instance.encodeForLDAP("Hi *", false)); + assertEquals("SLASH", "Hi \\2f", instance.encodeForLDAP("Hi /", false)); + assertEquals("ESC", "Hi \\5c", instance.encodeForLDAP("Hi \\", false)); + assertEquals("LDAP Christams Tree", "Hi \\28This\\29 = is * a \\5c test # \\ef\\bf\\bd \\ef\\bf\\bd \\ef\\bf\\bd", instance.encodeForLDAP("Hi (This) = is * a \\ test # � � �", false)); assertEquals("Forward slash for \\2fMicrosoft\\2f \\2fAD\\2f", instance.encodeForLDAP("Forward slash for /Microsoft/ /AD/")); + assertEquals("RFC 4515, Section 4", "(&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))", + "(&(objectClass=" + instance.encodeForLDAP("Person") + ")(|(sn=" + instance.encodeForLDAP("Jensen") + ")(cn=" + instance.encodeForLDAP("Babs J*", false) + ")))"); + assertEquals("RFC 4515, Section 4", "(o=univ*of*mich*)", + "(o=" + instance.encodeForLDAP("univ*of*mich*", false) + ")"); } /** * Test of encodeForDN method, of class org.owasp.esapi.Encoder. + * + * Additional tests: https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt */ public void testEncodeForDN() { System.out.println("encodeForDN"); Encoder instance = ESAPI.encoder(); assertEquals(null, instance.encodeForDN(null)); - assertEquals("No special characters to escape", "Hello�", instance.encodeForDN("Hello�")); - assertEquals("leading #", "\\# Hello�", instance.encodeForDN("# Hello�")); - assertEquals("leading space", "\\ Hello�", instance.encodeForDN(" Hello�")); - assertEquals("trailing space", "Hello�\\ ", instance.encodeForDN("Hello� ")); + assertEquals("No special characters to escape", "Hello", instance.encodeForDN("Hello")); + assertEquals("No special characters to escape", "Hello \u0007f", instance.encodeForDN("Hello \u0007f")); + assertEquals("Special characters to escape", "Hello \\c2\\80", instance.encodeForDN("Hello \u0080")); + assertEquals("Special characters to escape", "Hello \\c3\\bf", instance.encodeForDN("Hello \u00FF")); + assertEquals("Special characters to escape", "Hello \\df\\bf", instance.encodeForDN("Hello \u07FF")); + assertEquals("Special characters to escape", "Hello \\e0\\a0\\80", instance.encodeForDN("Hello \u0800")); + assertEquals("Special characters to escape", "Hello \\e0\\a3\\bf", instance.encodeForLDAP("Hello \u08FF")); + assertEquals("Special characters to escape", "Hello \\e7\\bf\\bf", instance.encodeForDN("Hello \u7FFF")); + assertEquals("Special characters to escape", "Hello \\e8\\80\\80", instance.encodeForDN("Hello \u8000")); + assertEquals("Special characters to escape", "Hello \\e8\\bf\\bf", instance.encodeForDN("Hello \u8FFF")); + assertEquals("Special characters to escape", "Hello \\ef\\bf\\bf", instance.encodeForDN("Hello \uFFFF")); + assertEquals("Special characters to escape", "Hello\\ef\\bf\\bd", instance.encodeForDN("Hello�")); + assertEquals("NUL", "Hi \\00", instance.encodeForDN("Hi \u0000")); + assertEquals("DQUOTE", "Hi \\\"", instance.encodeForDN("Hi \"")); + assertEquals("PLUS", "Hi \\+", instance.encodeForDN("Hi +")); + assertEquals("COMMA", "Hi \\,", instance.encodeForDN("Hi ,")); + assertEquals("SLASH", "Hi \\/", instance.encodeForDN("Hi /")); + assertEquals("SEMI", "Hi \\;", instance.encodeForDN("Hi ;")); + assertEquals("LANGLE", "Hi \\<", instance.encodeForDN("Hi <")); + assertEquals("RANGLE", "Hi \\>", instance.encodeForDN("Hi >")); + assertEquals("ESC", "Hi \\\\", instance.encodeForDN("Hi \\")); + assertEquals("leading #", "\\# Hello\\ef\\bf\\bd", instance.encodeForDN("# Hello�")); + assertEquals("leading space", "\\ Hello\\ef\\bf\\bd", instance.encodeForDN(" Hello�")); + assertEquals("trailing space", "Hello\\ef\\bf\\bd\\ ", instance.encodeForDN("Hello� ")); assertEquals("less than greater than", "Hello\\<\\>", instance.encodeForDN("Hello<>")); assertEquals("only 3 spaces", "\\ \\ ", instance.encodeForDN(" ")); assertEquals("Christmas Tree DN", "\\ Hello\\\\ \\+ \\, \\\"World\\\" \\;\\ ", instance.encodeForDN(" Hello\\ + , \"World\" ; ")); assertEquals("Forward slash for \\/Microsoft\\/ \\/AD\\/", instance.encodeForDN("Forward slash for /Microsoft/ /AD/")); + assertEquals("RFC 4514, Section 4", "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net", + "CN=" + instance.encodeForDN("James \"Jim\" Smith, III") + ",DC=" + instance.encodeForDN("example") + ",DC=" + instance.encodeForDN("net")); } /** From fea91b31babce48336d67cd5a66ab44611f22a7f Mon Sep 17 00:00:00 2001 From: Jeffrey Walton Date: Sun, 18 Jun 2023 21:35:44 -0400 Subject: [PATCH 2/2] Add additional LDAP encoder tests --- src/test/java/org/owasp/esapi/reference/EncoderTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/org/owasp/esapi/reference/EncoderTest.java b/src/test/java/org/owasp/esapi/reference/EncoderTest.java index 101840992..963939626 100644 --- a/src/test/java/org/owasp/esapi/reference/EncoderTest.java +++ b/src/test/java/org/owasp/esapi/reference/EncoderTest.java @@ -633,6 +633,8 @@ public void testEncodeForDN() { assertEquals("Forward slash for \\/Microsoft\\/ \\/AD\\/", instance.encodeForDN("Forward slash for /Microsoft/ /AD/")); assertEquals("RFC 4514, Section 4", "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net", "CN=" + instance.encodeForDN("James \"Jim\" Smith, III") + ",DC=" + instance.encodeForDN("example") + ",DC=" + instance.encodeForDN("net")); + assertEquals("RFC 4514, Section 4", "CN=Lu\\c4\\8di\\c4\\87", + "CN=" + instance.encodeForDN("\u004C\u0075\u010D\u0069\u0107")); } /**