@@ -4,6 +4,178 @@ const fs = require('fs');
44const port = 12345 ;
55const sslport = 12346 ;
66
7+ describe ( 'LDAP Injection Prevention' , ( ) => {
8+ describe ( 'escapeDN' , ( ) => {
9+ it ( 'should escape comma' , ( ) => {
10+ expect ( ldap . escapeDN ( 'admin,ou=evil' ) ) . toBe ( 'admin\\,ou\\=evil' ) ;
11+ } ) ;
12+
13+ it ( 'should escape equals sign' , ( ) => {
14+ expect ( ldap . escapeDN ( 'admin=evil' ) ) . toBe ( 'admin\\=evil' ) ;
15+ } ) ;
16+
17+ it ( 'should escape plus sign' , ( ) => {
18+ expect ( ldap . escapeDN ( 'admin+evil' ) ) . toBe ( 'admin\\+evil' ) ;
19+ } ) ;
20+
21+ it ( 'should escape less-than and greater-than signs' , ( ) => {
22+ expect ( ldap . escapeDN ( 'admin<evil>' ) ) . toBe ( 'admin\\<evil\\>' ) ;
23+ } ) ;
24+
25+ it ( 'should escape hash at start' , ( ) => {
26+ expect ( ldap . escapeDN ( '#admin' ) ) . toBe ( '\\#admin' ) ;
27+ } ) ;
28+
29+ it ( 'should escape semicolon' , ( ) => {
30+ expect ( ldap . escapeDN ( 'admin;evil' ) ) . toBe ( 'admin\\;evil' ) ;
31+ } ) ;
32+
33+ it ( 'should escape double quote' , ( ) => {
34+ expect ( ldap . escapeDN ( 'admin"evil' ) ) . toBe ( 'admin\\"evil' ) ;
35+ } ) ;
36+
37+ it ( 'should escape backslash' , ( ) => {
38+ expect ( ldap . escapeDN ( 'admin\\evil' ) ) . toBe ( 'admin\\\\evil' ) ;
39+ } ) ;
40+
41+ it ( 'should escape leading space' , ( ) => {
42+ expect ( ldap . escapeDN ( ' admin' ) ) . toBe ( '\\ admin' ) ;
43+ } ) ;
44+
45+ it ( 'should escape trailing space' , ( ) => {
46+ expect ( ldap . escapeDN ( 'admin ' ) ) . toBe ( 'admin\\ ' ) ;
47+ } ) ;
48+
49+ it ( 'should escape multiple special characters' , ( ) => {
50+ expect ( ldap . escapeDN ( 'admin,ou=evil+cn=x' ) ) . toBe ( 'admin\\,ou\\=evil\\+cn\\=x' ) ;
51+ } ) ;
52+
53+ it ( 'should not modify safe values' , ( ) => {
54+ expect ( ldap . escapeDN ( 'testuser' ) ) . toBe ( 'testuser' ) ;
55+ expect ( ldap . escapeDN ( 'john.doe' ) ) . toBe ( 'john.doe' ) ;
56+ expect ( ldap . escapeDN ( 'user123' ) ) . toBe ( 'user123' ) ;
57+ } ) ;
58+ } ) ;
59+
60+ describe ( 'escapeFilter' , ( ) => {
61+ it ( 'should escape asterisk' , ( ) => {
62+ expect ( ldap . escapeFilter ( '*' ) ) . toBe ( '\\2a' ) ;
63+ } ) ;
64+
65+ it ( 'should escape open parenthesis' , ( ) => {
66+ expect ( ldap . escapeFilter ( 'test(' ) ) . toBe ( 'test\\28' ) ;
67+ } ) ;
68+
69+ it ( 'should escape close parenthesis' , ( ) => {
70+ expect ( ldap . escapeFilter ( 'test)' ) ) . toBe ( 'test\\29' ) ;
71+ } ) ;
72+
73+ it ( 'should escape backslash' , ( ) => {
74+ expect ( ldap . escapeFilter ( 'test\\' ) ) . toBe ( 'test\\5c' ) ;
75+ } ) ;
76+
77+ it ( 'should escape null byte' , ( ) => {
78+ expect ( ldap . escapeFilter ( 'test\x00' ) ) . toBe ( 'test\\00' ) ;
79+ } ) ;
80+
81+ it ( 'should escape multiple special characters' , ( ) => {
82+ expect ( ldap . escapeFilter ( '*()\\' ) ) . toBe ( '\\2a\\28\\29\\5c' ) ;
83+ } ) ;
84+
85+ it ( 'should not modify safe values' , ( ) => {
86+ expect ( ldap . escapeFilter ( 'testuser' ) ) . toBe ( 'testuser' ) ;
87+ expect ( ldap . escapeFilter ( 'john.doe' ) ) . toBe ( 'john.doe' ) ;
88+ expect ( ldap . escapeFilter ( 'user123' ) ) . toBe ( 'user123' ) ;
89+ } ) ;
90+
91+ it ( 'should escape filter injection attempt with wildcard' , ( ) => {
92+ expect ( ldap . escapeFilter ( 'x)(|(objectClass=*)' ) ) . toBe ( 'x\\29\\28|\\28objectClass=\\2a\\29' ) ;
93+ } ) ;
94+ } ) ;
95+
96+ describe ( 'authData validation' , ( ) => {
97+ it ( 'should reject missing authData.id' , async done => {
98+ const server = await mockLdapServer ( port , 'uid=testuser, o=example' ) ;
99+ const options = {
100+ suffix : 'o=example' ,
101+ url : `ldap://localhost:${ port } ` ,
102+ dn : 'uid={{id}}, o=example' ,
103+ } ;
104+ try {
105+ await ldap . validateAuthData ( { password : 'secret' } , options ) ;
106+ fail ( 'Should have rejected missing id' ) ;
107+ } catch ( err ) {
108+ expect ( err . message ) . toBe ( 'LDAP: Wrong username or password' ) ;
109+ }
110+ server . close ( done ) ;
111+ } ) ;
112+
113+ it ( 'should reject non-string authData.id' , async done => {
114+ const server = await mockLdapServer ( port , 'uid=testuser, o=example' ) ;
115+ const options = {
116+ suffix : 'o=example' ,
117+ url : `ldap://localhost:${ port } ` ,
118+ dn : 'uid={{id}}, o=example' ,
119+ } ;
120+ try {
121+ await ldap . validateAuthData ( { id : 123 , password : 'secret' } , options ) ;
122+ fail ( 'Should have rejected non-string id' ) ;
123+ } catch ( err ) {
124+ expect ( err . message ) . toBe ( 'LDAP: Wrong username or password' ) ;
125+ }
126+ server . close ( done ) ;
127+ } ) ;
128+ } ) ;
129+
130+ describe ( 'DN injection prevention' , ( ) => {
131+ it ( 'should prevent DN injection via comma in authData.id' , async done => {
132+ // Mock server accepts the DN that would result from an unescaped injection
133+ const server = await mockLdapServer ( port , 'uid=admin,ou=admins,o=example' ) ;
134+ const options = {
135+ suffix : 'o=example' ,
136+ url : `ldap://localhost:${ port } ` ,
137+ dn : 'uid={{id}}, o=example' ,
138+ } ;
139+ // Attacker tries to inject additional DN components via comma
140+ // Without escaping: DN = uid=admin,ou=admins, o=example (3 RDNs) → matches mock
141+ // With escaping: DN = uid=admin\,ou=admins, o=example (2 RDNs) → doesn't match
142+ try {
143+ await ldap . validateAuthData ( { id : 'admin,ou=admins' , password : 'secret' } , options ) ;
144+ fail ( 'Should have rejected DN injection attempt' ) ;
145+ } catch ( err ) {
146+ expect ( err . message ) . toBe ( 'LDAP: Wrong username or password' ) ;
147+ }
148+ server . close ( done ) ;
149+ } ) ;
150+ } ) ;
151+
152+ describe ( 'Filter injection prevention' , ( ) => {
153+ it ( 'should prevent LDAP filter injection via wildcard in authData.id' , async done => {
154+ // Mock server accepts uid=*, o=example (the attacker's bind DN)
155+ // The * is not special in DNs so it binds fine regardless of escaping
156+ const server = await mockLdapServer ( port , 'uid=*, o=example' ) ;
157+ const options = {
158+ suffix : 'o=example' ,
159+ url : `ldap://localhost:${ port } ` ,
160+ dn : 'uid={{id}}, o=example' ,
161+ groupCn : 'powerusers' ,
162+ groupFilter : '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))' ,
163+ } ;
164+ // Attacker uses * as ID to match any group member via wildcard
165+ // Group has member uid=testuser, not uid=*
166+ // Without escaping: filter uses SubstringFilter, matches testuser → passes
167+ // With escaping: filter uses EqualityFilter with literal \2a, no match → fails
168+ try {
169+ await ldap . validateAuthData ( { id : '*' , password : 'secret' } , options ) ;
170+ fail ( 'Should have rejected filter injection attempt' ) ;
171+ } catch ( err ) {
172+ expect ( err . message ) . toBe ( 'LDAP: User not in group' ) ;
173+ }
174+ server . close ( done ) ;
175+ } ) ;
176+ } ) ;
177+ } ) ;
178+
7179describe ( 'Ldap Auth' , ( ) => {
8180 it ( 'Should fail with missing options' , done => {
9181 ldap
0 commit comments