@@ -217,6 +217,84 @@ def nosql_match(params):
217217 else : # $eq, $in (single-valued here) and any literal equality
218218 return record == value
219219
220+ # --- XPath endpoint (vulnerable search and login, backed by an in-memory XML document) ------------
221+
222+ XPATH_XML = """<?xml version="1.0" encoding="UTF-8"?>
223+ <directory>
224+ <department name="IT Operations">
225+ <user id="1">
226+ <username>luther</username>
227+ <realname>Luther Blisset</realname>
228+ <email>luther@example.com</email>
229+ <password>db3a16990a0008a3b04707fdef6584a0</password>
230+ <role>System Administrator</role>
231+ <location>London</location>
232+ <phone>+1 555 0100</phone>
233+ </user>
234+ <user id="2">
235+ <username>fluffy</username>
236+ <realname>Fluffy Bunny</realname>
237+ <email>fluffy@example.com</email>
238+ <password>4db967ce67b15e7fb84c266a76684729</password>
239+ <role>Security Engineer</role>
240+ <location>Amsterdam</location>
241+ <phone>+1 555 0102</phone>
242+ </user>
243+ <user id="3">
244+ <username>wu</username>
245+ <realname>Wu Ming</realname>
246+ <email>wu@example.com</email>
247+ <password>f5a2950eaa10f9e99896800eacbe8275</password>
248+ <role>Network Administrator</role>
249+ <location>Shanghai</location>
250+ <phone>+86 21 555 0103</phone>
251+ </user>
252+ </department>
253+ <department name="Engineering">
254+ <user id="4">
255+ <username>linus</username>
256+ <realname>Linus Torvalds</realname>
257+ <email>linus@example.com</email>
258+ <password>8e7b6a5c4d321908f7e6d5c4b3a2910f</password>
259+ <role>Kernel Developer</role>
260+ <location>Portland</location>
261+ <phone>+1 555 0200</phone>
262+ </user>
263+ <user id="5">
264+ <username>ada</username>
265+ <realname>Ada Lovelace</realname>
266+ <email>ada@example.com</email>
267+ <password>1a2b3c4d5e6f7081920a1b2c3d4e5f60</password>
268+ <role>Algorithm Designer</role>
269+ <location>London</location>
270+ <phone>+44 20 555 0201</phone>
271+ </user>
272+ </department>
273+ <department name="Management">
274+ <user id="6">
275+ <username>grace</username>
276+ <realname>Grace Hopper</realname>
277+ <email>grace@example.com</email>
278+ <password>9e8d7c6b5a493827160e9d8c7b6a5948</password>
279+ <role>CTO</role>
280+ <location>New York</location>
281+ <phone>+1 555 0300</phone>
282+ </user>
283+ </department>
284+ </directory>"""
285+
286+ def _xpath_element_to_dict (el ):
287+ """Convert an lxml element to a dict for JSON serialization."""
288+ retVal = dict (el .attrib )
289+ retVal ["tag" ] = el .tag
290+ retVal ["text" ] = (el .text or "" ).strip ()
291+ children = []
292+ for child in el :
293+ children .append (_xpath_element_to_dict (child ))
294+ if children :
295+ retVal ["children" ] = children
296+ return retVal
297+
220298_conn = None
221299_cursor = None
222300_lock = None
@@ -889,6 +967,58 @@ def do_REQUEST(self):
889967 self .wfile .write (output .encode (UNICODE_ENCODING ))
890968 return
891969
970+ if self .url == "/xpath/search" :
971+ self .send_response (OK )
972+ self .send_header ("Content-type" , "application/json; charset=%s" % UNICODE_ENCODING )
973+ self .send_header ("Connection" , "close" )
974+ self .end_headers ()
975+
976+ q = self .params .get ("q" , "" )
977+ entries = []
978+ error = None
979+
980+ if q :
981+ try :
982+ from lxml import etree
983+ root = etree .fromstring (XPATH_XML .encode ("utf-8" ))
984+ # VULNERABLE: unsanitized user input directly interpolated into XPath
985+ xpath_expr = "/directory/department/user[contains(username,'%s') or contains(realname,'%s')]" % (q , q )
986+ elements = root .xpath (xpath_expr )
987+ entries = [_xpath_element_to_dict (el ) for el in elements ]
988+ except Exception as ex :
989+ error = "%s: %s" % (type (ex ).__name__ , getUnicode (ex ))
990+
991+ output = json .dumps ({"entries" : entries , "count" : len (entries ), "error" : error }, default = str )
992+ self .wfile .write (output .encode (UNICODE_ENCODING ))
993+ return
994+
995+ if self .url == "/xpath/login" :
996+ self .send_response (OK )
997+ self .send_header ("Content-type" , "application/json; charset=%s" % UNICODE_ENCODING )
998+ self .send_header ("Connection" , "close" )
999+ self .end_headers ()
1000+
1001+ username = self .params .get ("username" , "" )
1002+ password = self .params .get ("password" , "" )
1003+ error = None
1004+ authenticated = False
1005+
1006+ if username and password :
1007+ try :
1008+ from lxml import etree
1009+ root = etree .fromstring (XPATH_XML .encode ("utf-8" ))
1010+ # VULNERABLE: unsanitized interpolation into XPath login expression
1011+ xpath_expr = "/directory/department/user[username='%s' and password='%s']" % (username , password )
1012+ results = root .xpath (xpath_expr )
1013+ if results :
1014+ authenticated = True
1015+ except Exception as ex :
1016+ error = "%s: %s" % (type (ex ).__name__ , getUnicode (ex ))
1017+
1018+ output = json .dumps ({"authenticated" : authenticated , "error" : error }, default = str )
1019+ self .wfile .write (output .encode (UNICODE_ENCODING ))
1020+ return
1021+
8921022 if self .url == '/' :
8931023 if not any (_ in self .params for _ in ("id" , "query" )):
8941024 self .send_response (OK )
0 commit comments