diff --git a/python/ql/src/experimental/Security/Practical-CodeQL-Introduction/query.ql b/python/ql/src/experimental/Security/Practical-CodeQL-Introduction/query.ql new file mode 100644 index 000000000000..f3e33b905085 --- /dev/null +++ b/python/ql/src/experimental/Security/Practical-CodeQL-Introduction/query.ql @@ -0,0 +1,3 @@ +import python + +select "Hello Wo... CodeQL!" diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Basic_approaches.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Basic_approaches.py new file mode 100644 index 000000000000..1dcd3463368a --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Basic_approaches.py @@ -0,0 +1,5 @@ +def basic(): + text = "this is a demo" + eval(text) + tixt = "second demo" + eval(tixt) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Code_Injection.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Code_Injection.py new file mode 100644 index 000000000000..3839ea8ed399 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Code_Injection.py @@ -0,0 +1,45 @@ +from flask import Flask, request + +app = Flask(__name__) + + +@app.route("/flow1") +def flow1(): + code = request.args["code"] + eval(code) + + +@app.route("/flow2") +def flow2(): + email = request.args["email"] + eval("./send_email {email}".format(email=email)) + + +def flow3_extra(text): + return text.split("\n") + + +@app.route("/flow3") +def flow3(): + text = request.args["text"] + eval(flow3_extra(text)) + + +@app.route("/flow4") +def flow4(): + text = request.args["text"] + tixt = text + toxt = flow3_extra(tixt) + tuxt = toxt + eval(tuxt) + + +@app.route("/flow1_good") +def flow1_good(): + code = request.args["code"] + if code == "print('Hello, Wo... CodeQL!')": + eval(code) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP3_Injection_bad.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP3_Injection_bad.py new file mode 100644 index 000000000000..2edb986fccfc --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP3_Injection_bad.py @@ -0,0 +1,41 @@ +from flask import request, Flask +import ldap3 + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is used directly as DN and search filter + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv, user=dn, auto_bind=True).search( + dn, search_filter) + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP3_Injection_good.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP3_Injection_good.py new file mode 100644 index 000000000000..bb2e6d7af83e --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP3_Injection_good.py @@ -0,0 +1,49 @@ +from flask import request, Flask +import ldap3 +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = escape_rdn(unsafe_dc) + safe_filter = escape_filter_chars(unsafe_filter) + + dn = "dc={}".format(safe_dc) + search_filter = "(user={})".format(safe_filter) + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = escape_rdn(unsafe_dc) + safe_filter = escape_filter_chars(unsafe_filter) + + dn = "dc={}".format(safe_dc) + search_filter = "(user={})".format(safe_filter) + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv, user=dn, auto_bind=True).search( + dn, search_filter) + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP_Injection_bad.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP_Injection_bad.py new file mode 100644 index 000000000000..133b0baaf9c0 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP_Injection_bad.py @@ -0,0 +1,59 @@ +from flask import request, Flask +import ldap + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is used directly as DN and search filter + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1") + user = ldap_connection.search_s( + dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search_s + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + user = ldap.initialize("ldap://127.0.0.1").search_s( + dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/normal_argbyname") +def normal_argbyname(): + """ + A RemoteFlowSource is used directly as DN and search filter, while the search filter is specified as + an argument by name + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1") + user = ldap_connection.search_s( + dn, ldap.SCOPE_SUBTREE, filterstr=search_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP_Injection_good.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP_Injection_good.py new file mode 100644 index 000000000000..dfc6f91d0455 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/LDAP_Injection_good.py @@ -0,0 +1,70 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + dn = "dc={}".format(safe_dc) + search_filter = "(user={})".format(safe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1") + user = ldap_connection.search_s( + dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search_s + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + dn = "dc={}".format(safe_dc) + search_filter = "(user={})".format(safe_filter) + + user = ldap.initialize("ldap://127.0.0.1").search_s( + dn, ldap.SCOPE_SUBTREE, search_filter, ["testAttr1", "testAttr2"]) + + +@app.route("/normal_argbyname") +def normal_argbyname(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter, while the search filter is specified as + an argument by name + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + dn = "dc={}".format(safe_dc) + search_filter = "(user={})".format(safe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1") + user = ldap_connection.search_s( + dn, ldap.SCOPE_SUBTREE, filterstr=search_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Practical-CodeQL-Introduction.expected b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Practical-CodeQL-Introduction.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Practical-CodeQL-Introduction.qlref b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Practical-CodeQL-Introduction.qlref new file mode 100644 index 000000000000..efdd25476224 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Practical-CodeQL-Introduction.qlref @@ -0,0 +1 @@ +experimental/Security/Practical-CodeQL-Introduction/query.ql \ No newline at end of file diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Regex_Injection_bad.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Regex_Injection_bad.py new file mode 100644 index 000000000000..622eaf199f6b --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Regex_Injection_bad.py @@ -0,0 +1,40 @@ +from flask import request, Flask +import re + +app = Flask(__name__) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is used directly as re.search's pattern + """ + + unsafe_pattern = request.args["pattern"] + re.search(unsafe_pattern, "") + + +@app.route("/compile") +def compile(): + """ + A RemoteFlowSource is used directly as re.compile's pattern + which also executes .search() + """ + + unsafe_pattern = request.args["pattern"] + compiled_pattern = re.compile(unsafe_pattern) + compiled_pattern.search("") + + +@app.route("/compile_direct") +def compile_direct(): + """ + A RemoteFlowSource is used directly as re.compile's pattern + which also executes .search() in the same line + """ + + unsafe_pattern = request.args["pattern"] + re.compile(unsafe_pattern).search("") + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Regex_Injection_good.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Regex_Injection_good.py new file mode 100644 index 000000000000..cdc9a7ac158c --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/Regex_Injection_good.py @@ -0,0 +1,17 @@ +from flask import request, Flask +import re + + +@app.route("/direct") +def direct(): + unsafe_pattern = request.args['pattern'] + safe_pattern = re.escape(unsafe_pattern) + re.search(safe_pattern, "") + + +@app.route("/compile") +def compile(): + unsafe_pattern = request.args['pattern'] + safe_pattern = re.escape(unsafe_pattern) + compiled_pattern = re.compile(safe_pattern) + compiled_pattern.search("") diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/XXE_general.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/XXE_general.py new file mode 100644 index 000000000000..c9f8cc984bda --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/XXE_general.py @@ -0,0 +1,73 @@ +from flask import request, Flask +from io import StringIO, BytesIO +import xml.etree +import xml.etree.ElementTree +import lxml.etree +import xml.dom.minidom +import xml.dom.pulldom +import xmltodict + + +app = Flask(__name__) + +# xml_content = ']>&xxe;' + + +@app.route("/lxml.etree.fromstring") +def lxml_fromstring(): + xml_content = request.args['xml_content'] + + return lxml.etree.fromstring(xml_content).text + + +@app.route("/lxml.etree.XML") +def lxml_XML(): + xml_content = request.args['xml_content'] + + return lxml.etree.XML(xml_content).text + + +@app.route("/lxml.etree.parse") +def lxml_parse(): + xml_content = request.args['xml_content'] + + return lxml.etree.parse(StringIO(xml_content)).text + + +@app.route("/xmltodict.parse") +def xmltodict_parse(): + xml_content = request.args['xml_content'] + + return xmltodict.parse(xml_content, disable_entities=False) + + +@app.route("/lxml.etree.XMLParser+lxml.etree.fromstring") +def lxml_XMLParser_fromstring(): + xml_content = request.args['xml_content'] + + parser = lxml.etree.XMLParser() + return lxml.etree.fromstring(xml_content, parser=parser).text + + +@app.route("/lxml.etree.get_default_parser+lxml.etree.fromstring") +def lxml_defaultParser_fromstring(): + xml_content = request.args['xml_content'] + + parser = lxml.etree.get_default_parser() + return lxml.etree.fromstring(xml_content, parser=parser).text + + +@app.route("/lxml.etree.XMLParser+xml.etree.ElementTree.fromstring") +def lxml_XMLParser_xml_fromstring(): + xml_content = request.args['xml_content'] + + parser = lxml.etree.XMLParser() + return xml.etree.ElementTree.fromstring(xml_content, parser=parser).text + + +@app.route("/lxml.etree.XMLParser+xml.etree.ElementTree.parse") +def lxml_XMLParser_xml_parse(): + xml_content = request.args['xml_content'] + + parser = lxml.etree.XMLParser() + return xml.etree.ElementTree.parse(StringIO(xml_content), parser=parser).getroot().text diff --git a/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/XXE_sax.py b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/XXE_sax.py new file mode 100644 index 000000000000..9a7bc0050f7e --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/Practical-CodeQL-Introduction/XXE_sax.py @@ -0,0 +1,75 @@ +from flask import request, Flask +from io import StringIO +import xml.sax + +# xml_content = ']>&xxe;' + +app = Flask(__name__) + + +class MainHandler(xml.sax.ContentHandler): + def __init__(self): + self._result = [] + + def characters(self, data): + self._result.append(data) + + def parse(self, f): + xml.sax.parse(f, self) + return self._result + +# GOOD + + +@app.route("/MainHandler") +def mainHandler(): + xml_content = request.args['xml_content'] + + return MainHandler().parse(StringIO(xml_content)) + + +@app.route("/xml.sax.make_parser()+MainHandler") +def xml_makeparser_MainHandler(): + xml_content = request.args['xml_content'] + + GoodHandler = MainHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(GoodHandler) + parser.parse(StringIO(xml_content)) + return GoodHandler._result + + +@app.route("/xml.sax.make_parser()+MainHandler-xml.sax.handler.feature_external_ges_False") +def xml_makeparser_MainHandler_entitiesFalse(): + xml_content = request.args['xml_content'] + + GoodHandler = MainHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(GoodHandler) + # https://docs.python.org/3/library/xml.sax.handler.html#xml.sax.handler.feature_external_ges + parser.setFeature(xml.sax.handler.feature_external_ges, False) + parser.parse(StringIO(xml_content)) + return GoodHandler._result + +# BAD + + +@app.route("/xml.sax.make_parser()+MainHandler-xml.sax.handler.feature_external_ges_True") +def xml_makeparser_MainHandler_entitiesTrue(): + xml_content = request.args['xml_content'] + + BadHandler = MainHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(BadHandler) + parser.setFeature(xml.sax.handler.feature_external_ges, True) + parser.parse(StringIO(xml_content)) + return BadHandler._result + + +@app.route("/xml.sax.make_parser()+xml.dom.minidom.parse-xml.sax.handler.feature_external_ges_True") +def xml_makeparser_minidom_entitiesTrue(): + xml_content = request.args['xml_content'] + + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + return xml.dom.minidom.parse(StringIO(xml_content), parser=parser).documentElement.childNodes