diff --git a/python/ql/src/experimental/Security/CWE-611/SimpleXmlRpcServer.ql b/python/ql/src/experimental/Security/CWE-611/SimpleXmlRpcServer.ql new file mode 100644 index 000000000000..4177daf29c10 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-611/SimpleXmlRpcServer.ql @@ -0,0 +1,25 @@ +/** + * @name SimpleXMLRPCServer DoS vulnerability + * @description SimpleXMLRPCServer is vulnerable to DoS attacks from untrusted user input + * @kind problem + * @problem.severity warning + * @precision high + * @id py/simple-xml-rpc-server-dos + * @tags security + * external/cwe/cwe-776 + */ + +private import python +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +from DataFlow::CallCfgNode call, string kinds +where + call = API::moduleImport("xmlrpc").getMember("server").getMember("SimpleXMLRPCServer").getACall() and + kinds = + strictconcat(XML::XMLVulnerabilityKind kind | + kind.isBillionLaughs() or kind.isQuadraticBlowup() + | + kind, ", " + ) +select call, "SimpleXMLRPCServer is vulnerable to: " + kinds + "." diff --git a/python/ql/src/experimental/Security/CWE-611/XmlEntityInjection.ql b/python/ql/src/experimental/Security/CWE-611/XmlEntityInjection.ql index 8f22ded4b157..922ca346b173 100644 --- a/python/ql/src/experimental/Security/CWE-611/XmlEntityInjection.ql +++ b/python/ql/src/experimental/Security/CWE-611/XmlEntityInjection.ql @@ -15,8 +15,17 @@ import python import experimental.semmle.python.security.dataflow.XmlEntityInjection import DataFlow::PathGraph -from DataFlow::PathNode source, DataFlow::PathNode sink, string kind -where XmlEntityInjection::xmlEntityInjectionVulnerable(source, sink, kind) +from + XmlEntityInjection::XmlEntityInjectionConfiguration config, DataFlow::PathNode source, + DataFlow::PathNode sink, string kinds +where + config.hasFlowPath(source, sink) and + kinds = + strictconcat(string kind | + kind = sink.getNode().(XmlEntityInjection::Sink).getVulnerableKind() + | + kind, ", " + ) select sink.getNode(), source, sink, - "$@ XML input is constructed from a $@ and is vulnerable to " + kind + ".", sink.getNode(), + "$@ XML input is constructed from a $@ and is vulnerable to: " + kinds + ".", sink.getNode(), "This", source.getNode(), "user-provided value" diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 09f8e7897c58..491267d057f2 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -16,69 +16,53 @@ private import experimental.semmle.python.Frameworks module XML { /** - * A data-flow node that collects functions parsing XML. + * A kind of XML vulnerability. * - * Extend this class to model new APIs. If you want to refine existing API models, - * extend `XMLParsing` instead. + * See https://pypi.org/project/defusedxml/#python-xml-libraries */ - class XMLParsing extends DataFlow::Node instanceof XMLParsing::Range { - /** - * Gets the argument containing the content to parse. - */ - DataFlow::Node getAnInput() { result = super.getAnInput() } + class XMLVulnerabilityKind extends string { + XMLVulnerabilityKind() { + this in ["Billion Laughs", "Quadratic Blowup", "XXE", "DTD retrieval"] + } - /** - * Holds if the parsing method or the parser holding it is vulnerable to `kind`. - */ - predicate vulnerable(string kind) { super.vulnerable(kind) } - } + /** Holds for Billion Laughs vulnerability kind. */ + predicate isBillionLaughs() { this = "Billion Laughs" } - /** Provides classes for modeling XML parsing APIs. */ - module XMLParsing { - /** - * A data-flow node that collects functions parsing XML. - * - * Extend this class to model new APIs. If you want to refine existing API models, - * extend `XMLParsing` instead. - */ - abstract class Range extends DataFlow::Node { - /** - * Gets the argument containing the content to parse. - */ - abstract DataFlow::Node getAnInput(); + /** Holds for Quadratic Blowup vulnerability kind. */ + predicate isQuadraticBlowup() { this = "Quadratic Blowup" } - /** - * Holds if the parsing method or the parser holding it is vulnerable to `kind`. - */ - abstract predicate vulnerable(string kind); - } + /** Holds for XXE vulnerability kind. */ + predicate isXxe() { this = "XXE" } + + /** Holds for DTD retrieval vulnerability kind. */ + predicate isDtdRetrieval() { this = "DTD retrieval" } } /** - * A data-flow node that collects XML parsers. + * A data-flow node that parses XML. * * Extend this class to model new APIs. If you want to refine existing API models, - * extend `XMLParser` instead. + * extend `XMLParsing` instead. */ - class XMLParser extends DataFlow::Node instanceof XMLParser::Range { + class XMLParsing extends DataFlow::Node instanceof XMLParsing::Range { /** * Gets the argument containing the content to parse. */ DataFlow::Node getAnInput() { result = super.getAnInput() } /** - * Holds if the parser is vulnerable to `kind`. + * Holds if this XML parsing is vulnerable to `kind`. */ - predicate vulnerable(string kind) { super.vulnerable(kind) } + predicate vulnerableTo(XMLVulnerabilityKind kind) { super.vulnerableTo(kind) } } - /** Provides classes for modeling XML parsers. */ - module XMLParser { + /** Provides classes for modeling XML parsing APIs. */ + module XMLParsing { /** - * A data-flow node that collects XML parsers. + * A data-flow node that parses XML. * * Extend this class to model new APIs. If you want to refine existing API models, - * extend `XMLParser` instead. + * extend `XMLParsing` instead. */ abstract class Range extends DataFlow::Node { /** @@ -87,9 +71,9 @@ module XML { abstract DataFlow::Node getAnInput(); /** - * Holds if the parser is vulnerable to `kind`. + * Holds if this XML parsing is vulnerable to `kind`. */ - abstract predicate vulnerable(string kind); + abstract predicate vulnerableTo(XMLVulnerabilityKind kind); } } } diff --git a/python/ql/src/experimental/semmle/python/frameworks/Xml.qll b/python/ql/src/experimental/semmle/python/frameworks/Xml.qll index cf4abbac995b..18ba6c5a572c 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Xml.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Xml.qll @@ -8,298 +8,457 @@ private import semmle.python.dataflow.new.DataFlow private import experimental.semmle.python.Concepts private import semmle.python.ApiGraphs -private module Xml { +private module XmlEtree { /** - * Gets a call to `xml.etree.ElementTree.XMLParser`. + * Provides models for `xml.etree` parsers + * + * See + * - https://docs.python.org/3.10/library/xml.etree.elementtree.html#xml.etree.ElementTree.XMLParser + * - https://docs.python.org/3.10/library/xml.etree.elementtree.html#xml.etree.ElementTree.XMLPullParser + */ + module XMLParser { + /** + * A source of instances of `xml.etree` parsers, extend this class to model new instances. + * + * This can include instantiations of the class, return values from function + * calls, or a special parameter that will be set when functions are called by an external + * library. + * + * Use the predicate `XMLParser::instance()` to get references to instances of `xml.etree` parsers. + */ + abstract class InstanceSource extends DataFlow::LocalSourceNode { } + + /** A direct instantiation of `xml.etree` parsers. */ + private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode { + ClassInstantiation() { + this = + API::moduleImport("xml") + .getMember("etree") + .getMember("ElementTree") + .getMember("XMLParser") + .getACall() + or + this = + API::moduleImport("xml") + .getMember("etree") + .getMember("ElementTree") + .getMember("XMLPullParser") + .getACall() + } + } + + /** Gets a reference to an `xml.etree` parser instance. */ + private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) { + t.start() and + result instanceof InstanceSource + or + exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t)) + } + + /** Gets a reference to an `xml.etree` parser instance. */ + DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) } + + /** + * A call to the `feed` method of an `xml.etree` parser. + */ + private class XMLEtreeParserFeedCall extends DataFlow::MethodCallNode, XML::XMLParsing::Range { + XMLEtreeParserFeedCall() { this.calls(instance(), "feed") } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("data")] } + + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + kind.isBillionLaughs() or kind.isQuadraticBlowup() + } + } + } + + /** + * A call to either of: + * - `xml.etree.ElementTree.fromstring` + * - `xml.etree.ElementTree.fromstringlist` + * - `xml.etree.ElementTree.XML` + * - `xml.etree.ElementTree.XMLID` + * - `xml.etree.ElementTree.parse` + * - `xml.etree.ElementTree.iterparse` */ - private class XMLEtreeParser extends DataFlow::CallCfgNode, XML::XMLParser::Range { - XMLEtreeParser() { + private class XMLEtreeParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { + XMLEtreeParsing() { this = API::moduleImport("xml") .getMember("etree") .getMember("ElementTree") - .getMember("XMLParser") + .getMember(["fromstring", "fromstringlist", "XML", "XMLID", "parse", "iterparse"]) .getACall() } - override DataFlow::Node getAnInput() { none() } + override DataFlow::Node getAnInput() { + result in [ + this.getArg(0), + // fromstring / XML / XMLID + this.getArgByName("text"), + // fromstringlist + this.getArgByName("sequence"), + // parse / iterparse + this.getArgByName("source"), + ] + } - override predicate vulnerable(string kind) { none() } + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + // note: it does not matter what `xml.etree` parser you are using, you cannot + // change the security features anyway :| + kind.isBillionLaughs() or kind.isQuadraticBlowup() + } } +} +private module SaxBasedParsing { /** - * Gets a call to: - * * `xml.etree.ElementTree.fromstring` - * * `xml.etree.ElementTree.fromstringlist` - * * `xml.etree.ElementTree.XML` - * * `xml.etree.ElementTree.parse` - * - * Given the following example: - * - * ```py - * parser = lxml.etree.XMLParser() - * xml.etree.ElementTree.fromstring(xml_content, parser=parser).text - * ``` + * A call to the `setFeature` method on a XML sax parser. * - * * `this` would be `xml.etree.ElementTree.fromstring(xml_content, parser=parser)`. - * * `getAnInput()`'s result would be `xml_content`. - * * `vulnerable(kind)`'s `kind` would be `XXE`. + * See https://docs.python.org/3.10/library/xml.sax.reader.html#xml.sax.xmlreader.XMLReader.setFeature */ - private class XMLEtreeParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { - XMLEtreeParsing() { + class SaxParserSetFeatureCall extends DataFlow::MethodCallNode { + SaxParserSetFeatureCall() { this = API::moduleImport("xml") - .getMember("etree") - .getMember("ElementTree") - .getMember(["fromstring", "fromstringlist", "XML", "parse"]) + .getMember("sax") + .getMember("make_parser") + .getReturn() + .getMember("setFeature") .getACall() } - override DataFlow::Node getAnInput() { result = this.getArg(0) } + // The keyword argument names does not match documentation. I checked (with Python + // 3.9.5) that the names used here actually works. + DataFlow::Node getFeatureArg() { result in [this.getArg(0), this.getArgByName("name")] } - override predicate vulnerable(string kind) { - exists(XML::XMLParser xmlParser | - xmlParser = this.getArgByName("parser").getALocalSource() and xmlParser.vulnerable(kind) - ) - } + DataFlow::Node getStateArg() { result in [this.getArg(1), this.getArgByName("state")] } } - /** Gets a reference to a `parser` that has been set a `feature`. */ - private DataFlow::Node trackSaxFeature( - DataFlow::TypeTracker t, DataFlow::CallCfgNode parser, API::Node feature + /** Gets a back-reference to the `setFeature` state argument `arg`. */ + private DataFlow::TypeTrackingNode saxParserSetFeatureStateArgBacktracker( + DataFlow::TypeBackTracker t, DataFlow::Node arg ) { t.start() and - exists(DataFlow::MethodCallNode featureCall | - featureCall = parser.getAMethodCall("setFeature") and - featureCall.getArg(0).getALocalSource() = feature.getAUse() and - featureCall.getArg(1).getALocalSource() = DataFlow::exprNode(any(True t_)) and - result = featureCall.getObject() - ) + arg = any(SaxParserSetFeatureCall c).getStateArg() and + result = arg.getALocalSource() or - exists(DataFlow::TypeTracker t2 | - t = t2.smallstep(trackSaxFeature(t2, parser, feature), result) + exists(DataFlow::TypeBackTracker t2 | + result = saxParserSetFeatureStateArgBacktracker(t2, arg).backtrack(t2, t) ) } - /** Gets a reference to a `parser` that has been set a `feature`. */ - DataFlow::Node trackSaxFeature(DataFlow::CallCfgNode parser, API::Node feature) { - result = trackSaxFeature(DataFlow::TypeTracker::end(), parser, feature) + /** Gets a back-reference to the `setFeature` state argument `arg`. */ + DataFlow::LocalSourceNode saxParserSetFeatureStateArgBacktracker(DataFlow::Node arg) { + result = saxParserSetFeatureStateArgBacktracker(DataFlow::TypeBackTracker::end(), arg) } /** - * Gets a call to `xml.sax.make_parser`. - * - * Given the following example: + * Gets a reference to a XML sax parser that has `feature_external_ges` turned on. * - * ```py - * BadHandler = MainHandler() - * parser = xml.sax.make_parser() - * parser.setContentHandler(BadHandler) - * parser.setFeature(xml.sax.handler.feature_external_ges, False) - * parser.parse(StringIO(xml_content)) - * parsed_xml = BadHandler._result - * ``` - * - * * `this` would be `xml.sax.make_parser()`. - * * `getAnInput()`'s result would be `StringIO(xml_content)`. - * * `vulnerable(kind)`'s `kind` would be `Billion Laughs` and `Quadratic Blowup`. + * See https://docs.python.org/3/library/xml.sax.handler.html#xml.sax.handler.feature_external_ges */ - private class XMLSaxParser extends DataFlow::CallCfgNode, XML::XMLParser::Range { - XMLSaxParser() { - this = API::moduleImport("xml").getMember("sax").getMember("make_parser").getACall() - } + private DataFlow::Node saxParserWithFeatureExternalGesTurnedOn(DataFlow::TypeTracker t) { + t.start() and + exists(SaxParserSetFeatureCall call | + call.getFeatureArg() = + API::moduleImport("xml") + .getMember("sax") + .getMember("handler") + .getMember("feature_external_ges") + .getAUse() and + saxParserSetFeatureStateArgBacktracker(call.getStateArg()) + .asExpr() + .(BooleanLiteral) + .booleanValue() = true and + result = call.getObject() + ) + or + exists(DataFlow::TypeTracker t2 | + t = t2.smallstep(saxParserWithFeatureExternalGesTurnedOn(t2), result) + ) and + // take account of that we can set the feature to False, which makes the parser safe again + not exists(SaxParserSetFeatureCall call | + call.getObject() = result and + call.getFeatureArg() = + API::moduleImport("xml") + .getMember("sax") + .getMember("handler") + .getMember("feature_external_ges") + .getAUse() and + saxParserSetFeatureStateArgBacktracker(call.getStateArg()) + .asExpr() + .(BooleanLiteral) + .booleanValue() = false + ) + } - override DataFlow::Node getAnInput() { result = this.getAMethodCall("parse").getArg(0) } + /** + * Gets a reference to a XML sax parser that has `feature_external_ges` turned on. + * + * See https://docs.python.org/3/library/xml.sax.handler.html#xml.sax.handler.feature_external_ges + */ + DataFlow::Node saxParserWithFeatureExternalGesTurnedOn() { + result = saxParserWithFeatureExternalGesTurnedOn(DataFlow::TypeTracker::end()) + } - override predicate vulnerable(string kind) { - exists(DataFlow::MethodCallNode parse, API::Node handler, API::Node feature | - handler = API::moduleImport("xml").getMember("sax").getMember("handler") and - parse.calls(trackSaxFeature(this, feature), "parse") and - parse.getArg(0) = this.getAnInput() // enough to avoid FPs? - | - kind = ["XXE", "DTD retrieval"] and - feature = handler.getMember("feature_external_ges") - or - kind = ["Billion Laughs", "Quadratic Blowup"] - ) + /** + * A call to the `parse` method on a SAX XML parser. + */ + private class XMLSaxInstanceParsing extends DataFlow::MethodCallNode, XML::XMLParsing::Range { + XMLSaxInstanceParsing() { + this = + API::moduleImport("xml") + .getMember("sax") + .getMember("make_parser") + .getReturn() + .getMember("parse") + .getACall() } - predicate vulnerable(DataFlow::Node n, string kind) { - exists(API::Node handler, API::Node feature | - handler = API::moduleImport("xml").getMember("sax").getMember("handler") and - DataFlow::exprNode(trackSaxFeature(this, feature).asExpr()) - .(DataFlow::LocalSourceNode) - .flowsTo(n) - | - kind = ["XXE", "DTD retrieval"] and - feature = handler.getMember("feature_external_ges") - ) + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("source")] } + + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + // always vuln to these + (kind.isBillionLaughs() or kind.isQuadraticBlowup()) + or + // can be vuln to other things if features has been turned on + this.getObject() = saxParserWithFeatureExternalGesTurnedOn() and + (kind.isXxe() or kind.isDtdRetrieval()) } } /** - * Gets a call to: - * * `lxml.etree.XMLParser` - * * `lxml.etree.get_default_parser` - * - * Given the following example: - * - * ```py - * lxml.etree.XMLParser() - * ``` + * A call to either `parse` or `parseString` from `xml.sax` module. * - * * `this` would be `lxml.etree.XMLParser(resolve_entities=False)`. - * * `vulnerable(kind)`'s `kind` would be `XXE` + * See: + * - https://docs.python.org/3.10/library/xml.sax.html#xml.sax.parse + * - https://docs.python.org/3.10/library/xml.sax.html#xml.sax.parseString */ - private class LXMLParser extends DataFlow::CallCfgNode, XML::XMLParser::Range { - LXMLParser() { + private class XMLSaxParsing extends DataFlow::MethodCallNode, XML::XMLParsing::Range { + XMLSaxParsing() { this = - API::moduleImport("lxml") - .getMember("etree") - .getMember(["XMLParser", "get_default_parser"]) - .getACall() + API::moduleImport("xml").getMember("sax").getMember(["parse", "parseString"]).getACall() } - override DataFlow::Node getAnInput() { none() } + override DataFlow::Node getAnInput() { + result in [ + this.getArg(0), + // parseString + this.getArgByName("string"), + // parse + this.getArgByName("source"), + ] + } - override predicate vulnerable(string kind) { - kind = "XXE" and - not ( - exists(this.getArgByName("resolve_entities")) or - this.getArgByName("resolve_entities").getALocalSource().asExpr() = any(False f) - ) + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + // always vuln to these + (kind.isBillionLaughs() or kind.isQuadraticBlowup()) or - kind = ["Billion Laughs", "Quadratic Blowup"] and - ( - this.getArgByName("huge_tree").getALocalSource().asExpr() = any(True t) and - not this.getArgByName("resolve_entities").getALocalSource().asExpr() = any(False f) - ) + // can be vuln to other things if features has been turned on + this.getObject() = saxParserWithFeatureExternalGesTurnedOn() and + (kind.isXxe() or kind.isDtdRetrieval()) } } /** - * Gets a call to: - * * `lxml.etree.fromstring` - * * `xml.etree.fromstringlist` - * * `xml.etree.XML` - * * `xml.etree.parse` - * - * Given the following example: + * A call to the `parse` or `parseString` methods from `xml.dom.minidom` or `xml.dom.pulldom`. * - * ```py - * parser = lxml.etree.XMLParser() - * lxml.etree.fromstring(xml_content, parser=parser).text - * ``` - * - * * `this` would be `lxml.etree.fromstring(xml_content, parser=parser)`. - * * `getAnInput()`'s result would be `xml_content`. - * * `vulnerable(kind)`'s `kind` would be `XXE`. + * Both of these modules are based on SAX parsers. */ - private class LXMLParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { - LXMLParsing() { + private class XMLDomParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { + XMLDomParsing() { this = - API::moduleImport("lxml") - .getMember("etree") - .getMember(["fromstring", "fromstringlist", "XML", "parse"]) + API::moduleImport("xml") + .getMember("dom") + .getMember(["minidom", "pulldom"]) + .getMember(["parse", "parseString"]) .getACall() } - override DataFlow::Node getAnInput() { result = this.getArg(0) } + override DataFlow::Node getAnInput() { + result in [ + this.getArg(0), + // parseString + this.getArgByName("string"), + // minidom.parse + this.getArgByName("file"), + // pulldom.parse + this.getArgByName("stream_or_string"), + ] + } + + DataFlow::Node getParserArg() { result in [this.getArg(1), this.getArgByName("parser")] } - override predicate vulnerable(string kind) { - exists(XML::XMLParser xmlParser | - xmlParser = this.getArgByName("parser").getALocalSource() and xmlParser.vulnerable(kind) - ) + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + this.getParserArg() = saxParserWithFeatureExternalGesTurnedOn() and + (kind.isXxe() or kind.isDtdRetrieval()) or - kind = "XXE" and not exists(this.getArgByName("parser")) + (kind.isBillionLaughs() or kind.isQuadraticBlowup()) } } +} +private module Lxml { /** - * Gets a call to `xmltodict.parse`. - * - * Given the following example: + * Provides models for `lxml.etree` parsers. * - * ```py - * xmltodict.parse(xml_content, disable_entities=False) - * ``` - * - * * `this` would be `xmltodict.parse(xml_content, disable_entities=False)`. - * * `getAnInput()`'s result would be `xml_content`. - * * `vulnerable(kind)`'s `kind` would be `Billion Laughs` and `Quadratic Blowup`. + * See https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser */ - private class XMLtoDictParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { - XMLtoDictParsing() { this = API::moduleImport("xmltodict").getMember("parse").getACall() } + module XMLParser { + /** + * A source of instances of `lxml.etree` parsers, extend this class to model new instances. + * + * This can include instantiations of the class, return values from function + * calls, or a special parameter that will be set when functions are called by an external + * library. + * + * Use the predicate `XMLParser::instance()` to get references to instances of `lxml.etree` parsers. + */ + abstract class InstanceSource extends DataFlow::LocalSourceNode { + /** Holds if this instance is vulnerable to `kind`. */ + abstract predicate vulnerableTo(XML::XMLVulnerabilityKind kind); + } - override DataFlow::Node getAnInput() { result = this.getArg(0) } + /** + * A call to `lxml.etree.XMLParser`. + * + * See https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser + */ + private class LXMLParser extends InstanceSource, DataFlow::CallCfgNode { + LXMLParser() { + this = API::moduleImport("lxml").getMember("etree").getMember("XMLParser").getACall() + } - override predicate vulnerable(string kind) { - kind = ["Billion Laughs", "Quadratic Blowup"] and - this.getArgByName("disable_entities").getALocalSource().asExpr() = any(False f) + // NOTE: it's not possible to change settings of a parser after constructing it + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + kind.isXxe() and + ( + // resolve_entities has default True + not exists(this.getArgByName("resolve_entities")) + or + this.getArgByName("resolve_entities").getALocalSource().asExpr() = any(True t) + ) + or + (kind.isBillionLaughs() or kind.isQuadraticBlowup()) and + this.getArgByName("huge_tree").getALocalSource().asExpr() = any(True t) and + not this.getArgByName("resolve_entities").getALocalSource().asExpr() = any(False t) + or + kind.isDtdRetrieval() and + this.getArgByName("load_dtd").getALocalSource().asExpr() = any(True t) and + this.getArgByName("no_network").getALocalSource().asExpr() = any(False t) + } + } + + /** + * A call to `lxml.etree.get_default_parser`. + * + * See https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.get_default_parser + */ + private class LXMLDefaultParser extends InstanceSource, DataFlow::CallCfgNode { + LXMLDefaultParser() { + this = + API::moduleImport("lxml").getMember("etree").getMember("get_default_parser").getACall() + } + + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + // as highlighted by + // https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser + // by default XXE is allow. so as long as the default parser has not been + // overridden, the result is also vuln to XXE. + kind.isXxe() + // TODO: take into account that you can override the default parser with `lxml.etree.set_default_parser`. + } + } + + /** Gets a reference to an `lxml.etree` parsers instance, with origin in `origin` */ + private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t, InstanceSource origin) { + t.start() and + result = origin + or + exists(DataFlow::TypeTracker t2 | result = instance(t2, origin).track(t2, t)) + } + + /** Gets a reference to an `lxml.etree` parsers instance, with origin in `origin` */ + DataFlow::Node instance(InstanceSource origin) { + instance(DataFlow::TypeTracker::end(), origin).flowsTo(result) + } + + /** Gets a reference to an `lxml.etree` parser instance, that is vulnerable to `kind`. */ + DataFlow::Node instanceVulnerableTo(XML::XMLVulnerabilityKind kind) { + exists(InstanceSource origin | result = instance(origin) and origin.vulnerableTo(kind)) + } + + /** + * A call to the `feed` method of an `lxml` parser. + */ + private class LXMLParserFeedCall extends DataFlow::MethodCallNode, XML::XMLParsing::Range { + LXMLParserFeedCall() { this.calls(instance(_), "feed") } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("data")] } + + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + this.calls(instanceVulnerableTo(kind), "feed") + } } } /** - * Gets a call to: - * * `xml.dom.minidom.parse` - * * `xml.dom.pulldom.parse` - * - * Given the following example: - * - * ```py - * xml.dom.minidom.parse(StringIO(xml_content)).documentElement.childNode - * ``` + * A call to either of: + * - `lxml.etree.fromstring` + * - `lxml.etree.fromstringlist` + * - `lxml.etree.XML` + * - `lxml.etree.parse` + * - `lxml.etree.parseid` * - * * `this` would be `xml.dom.minidom.parse(StringIO(xml_content), parser=parser)`. - * * `getAnInput()`'s result would be `StringIO(xml_content)`. - * * `vulnerable(kind)`'s `kind` would be `Billion Laughs` and `Quadratic Blowup`. + * See https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.fromstring */ - private class XMLDomParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { - XMLDomParsing() { + private class LXMLParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { + LXMLParsing() { this = - API::moduleImport("xml") - .getMember("dom") - .getMember(["minidom", "pulldom"]) - .getMember(["parse", "parseString"]) + API::moduleImport("lxml") + .getMember("etree") + .getMember(["fromstring", "fromstringlist", "XML", "parse", "parseid"]) .getACall() } - override DataFlow::Node getAnInput() { result = this.getArg(0) } + override DataFlow::Node getAnInput() { + result in [ + this.getArg(0), + // fromstring / XML + this.getArgByName("text"), + // fromstringlist + this.getArgByName("strings"), + // parse / parseid + this.getArgByName("source"), + ] + } + + DataFlow::Node getParserArg() { result in [this.getArg(1), this.getArgByName("parser")] } - override predicate vulnerable(string kind) { - exists(XML::XMLParser xmlParser | - xmlParser = this.getArgByName("parser").getALocalSource() and xmlParser.vulnerable(kind) - ) + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + this.getParserArg() = XMLParser::instanceVulnerableTo(kind) or - kind = ["Billion Laughs", "Quadratic Blowup"] and not exists(this.getArgByName("parser")) + kind.isXxe() and + not exists(this.getParserArg()) } } +} +private module Xmltodict { /** - * Gets a call to `xmlrpc.server.SimpleXMLRPCServer`. - * - * Given the following example: - * - * ```py - * server = SimpleXMLRPCServer(("127.0.0.1", 8000)) - * server.register_function(foo, "foo") - * server.serve_forever() - * ``` - * - * * `this` would be `SimpleXMLRPCServer(("127.0.0.1", 8000))`. - * * `getAnInput()`'s result would be `foo`. - * * `vulnerable(kind)`'s `kind` would be `Billion Laughs` and `Quadratic Blowup`. + * A call to `xmltodict.parse`. */ - private class XMLRPCServer extends DataFlow::CallCfgNode, XML::XMLParser::Range { - XMLRPCServer() { - this = - API::moduleImport("xmlrpc").getMember("server").getMember("SimpleXMLRPCServer").getACall() - } + private class XMLtoDictParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range { + XMLtoDictParsing() { this = API::moduleImport("xmltodict").getMember("parse").getACall() } override DataFlow::Node getAnInput() { - result = this.getAMethodCall("register_function").getArg(0) + result in [this.getArg(0), this.getArgByName("xml_input")] } - override predicate vulnerable(string kind) { kind = ["Billion Laughs", "Quadratic Blowup"] } + override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) { + (kind.isBillionLaughs() or kind.isQuadraticBlowup()) and + this.getArgByName("disable_entities").getALocalSource().asExpr() = any(False f) + } } } diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjection.qll b/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjection.qll index 087c3057640e..35220e153d12 100644 --- a/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjection.qll +++ b/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjection.qll @@ -25,22 +25,4 @@ module XmlEntityInjection { any(AdditionalTaintStep s).step(nodeFrom, nodeTo) } } - - private import DataFlow::PathGraph - - /** Holds if there is an XML injection from `source` to `sink` */ - predicate xmlEntityInjection(DataFlow::PathNode source, DataFlow::PathNode sink) { - any(XmlEntityInjectionConfiguration x).hasFlowPath(source, sink) - } - - /** Holds if there is an XML injection from `source` to `sink` vulnerable to `kind` */ - predicate xmlEntityInjectionVulnerable( - DataFlow::PathNode source, DataFlow::PathNode sink, string kind - ) { - xmlEntityInjection(source, sink) and - ( - xmlParsingInputAsVulnerableSink(sink.getNode(), kind) or - xmlParserInputAsVulnerableSink(sink.getNode(), kind) - ) - } } diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjectionCustomizations.qll b/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjectionCustomizations.qll index 8f8b3ae2c6ab..745658bbce7b 100644 --- a/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjectionCustomizations.qll +++ b/python/ql/src/experimental/semmle/python/security/dataflow/XmlEntityInjectionCustomizations.qll @@ -24,7 +24,10 @@ module XmlEntityInjection { /** * A data flow sink for "xml injection" vulnerabilities. */ - abstract class Sink extends DataFlow::Node { } + abstract class Sink extends DataFlow::Node { + /** Gets the kind of XML injection that this sink is vulnerable to. */ + abstract string getVulnerableKind(); + } /** * A sanitizer guard for "xml injection" vulnerabilities. @@ -46,53 +49,22 @@ module XmlEntityInjection { } /** - * A data flow sink for XML parsing libraries. + * An input to a direct XML parsing function, considered as a flow sink. * * See `XML::XMLParsing`. */ - abstract class XMLParsingSink extends Sink { } - - /** - * A data flow sink for XML parsers. - * - * See `XML::XMLParser` - */ - abstract class XMLParserSink extends Sink { } - - /** - * A source of remote user input, considered as a flow source. - */ - class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { } - - /** - * An xml parsing operation, considered as a flow sink. - */ - class XMLParsingInputAsSink extends XMLParsingSink { - XMLParsingInputAsSink() { this = any(XML::XMLParsing xmlParsing).getAnInput() } - } + class XMLParsingInputAsSink extends Sink { + XML::XMLParsing xmlParsing; - /** - * An xml parsing operation vulnerable to `kind`. - */ - predicate xmlParsingInputAsVulnerableSink(DataFlow::Node sink, string kind) { - exists(XML::XMLParsing xmlParsing | - sink = xmlParsing.getAnInput() and xmlParsing.vulnerable(kind) - ) - } + XMLParsingInputAsSink() { this = xmlParsing.getAnInput() } - /** - * An xml parser operation, considered as a flow sink. - */ - class XMLParserInputAsSink extends XMLParserSink { - XMLParserInputAsSink() { this = any(XML::XMLParser xmlParser).getAnInput() } + override string getVulnerableKind() { xmlParsing.vulnerableTo(result) } } /** - * An xml parser operation vulnerable to `kind`. + * A source of remote user input, considered as a flow source. */ - predicate xmlParserInputAsVulnerableSink(DataFlow::Node sink, string kind) { - exists(XML::XMLParser xmlParser | sink = xmlParser.getAnInput() and xmlParser.vulnerable(kind)) - } + class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { } /** * A comparison with a constant string, considered as a sanitizer-guard. diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/ExperimentalXmlConceptsTests.expected b/python/ql/test/experimental/library-tests/frameworks/XML/ExperimentalXmlConceptsTests.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/ExperimentalXmlConceptsTests.ql b/python/ql/test/experimental/library-tests/frameworks/XML/ExperimentalXmlConceptsTests.ql new file mode 100644 index 000000000000..81bc391d0e55 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/ExperimentalXmlConceptsTests.ql @@ -0,0 +1,33 @@ +import python +import experimental.semmle.python.Concepts +import experimental.semmle.python.frameworks.Xml +import semmle.python.dataflow.new.DataFlow +import TestUtilities.InlineExpectationsTest +private import semmle.python.dataflow.new.internal.PrintNode + +class XmlParsingTest extends InlineExpectationsTest { + XmlParsingTest() { this = "XmlParsingTest" } + + override string getARelevantTag() { result in ["input", "vuln"] } + + override predicate hasActualResult(Location location, string element, string tag, string value) { + exists(location.getFile().getRelativePath()) and + exists(XML::XMLParsing parsing | + exists(DataFlow::Node input | + input = parsing.getAnInput() and + location = input.getLocation() and + element = input.toString() and + value = prettyNodeForInlineTest(input) and + tag = "input" + ) + or + exists(XML::XMLVulnerabilityKind kind | + parsing.vulnerableTo(kind) and + location = parsing.getLocation() and + element = parsing.toString() and + value = "'" + kind + "'" and + tag = "vuln" + ) + ) + } +} diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/lxml_etree.py b/python/ql/test/experimental/library-tests/frameworks/XML/lxml_etree.py new file mode 100644 index 000000000000..22930a58af37 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/lxml_etree.py @@ -0,0 +1,54 @@ +from io import StringIO +import lxml.etree + +x = "some xml" + +# different parsing methods +lxml.etree.fromstring(x) # $ input=x vuln='XXE' +lxml.etree.fromstring(text=x) # $ input=x vuln='XXE' + +lxml.etree.fromstringlist([x]) # $ input=List vuln='XXE' +lxml.etree.fromstringlist(strings=[x]) # $ input=List vuln='XXE' + +lxml.etree.XML(x) # $ input=x vuln='XXE' +lxml.etree.XML(text=x) # $ input=x vuln='XXE' + +lxml.etree.parse(StringIO(x)) # $ input=StringIO(..) vuln='XXE' +lxml.etree.parse(source=StringIO(x)) # $ input=StringIO(..) vuln='XXE' + +lxml.etree.parseid(StringIO(x)) # $ input=StringIO(..) vuln='XXE' +lxml.etree.parseid(source=StringIO(x)) # $ input=StringIO(..) vuln='XXE' + +# With default parsers (nothing changed) +parser = lxml.etree.XMLParser() +lxml.etree.fromstring(x, parser=parser) # $ input=x vuln='XXE' + +parser = lxml.etree.get_default_parser() +lxml.etree.fromstring(x, parser=parser) # $ input=x vuln='XXE' + +# manual use of feed method +parser = lxml.etree.XMLParser() +parser.feed(x) # $ input=x vuln='XXE' +parser.feed(data=x) # $ input=x vuln='XXE' +parser.close() + +# XXE-safe +parser = lxml.etree.XMLParser(resolve_entities=False) +lxml.etree.fromstring(x, parser) # $ input=x +lxml.etree.fromstring(x, parser=parser) # $ input=x + +# XXE-vuln +parser = lxml.etree.XMLParser(resolve_entities=True) +lxml.etree.fromstring(x, parser=parser) # $ input=x vuln='XXE' + +# Billion laughs vuln (also XXE) +parser = lxml.etree.XMLParser(huge_tree=True) +lxml.etree.fromstring(x, parser=parser) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' vuln='XXE' + +# Safe for both Billion laughs and XXE +parser = lxml.etree.XMLParser(resolve_entities=False, huge_tree=True) +lxml.etree.fromstring(x, parser=parser) # $ input=x + +# DTD retrival vuln (also XXE) +parser = lxml.etree.XMLParser(load_dtd=True, no_network=False) +lxml.etree.fromstring(x, parser=parser) # $ input=x vuln='DTD retrieval' vuln='XXE' diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/poc/PoC.py b/python/ql/test/experimental/library-tests/frameworks/XML/poc/PoC.py new file mode 100644 index 000000000000..adcace1aa0a6 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/poc/PoC.py @@ -0,0 +1,677 @@ +#!/usr/bin/env python3 + +# this file doesn't have a .py extension so the extractor doesn't pick it up, so it +# doesn't have to be annotated + +# This file shows the ways to make exploit vulnerable XML parsing +# see +# https://pypi.org/project/defusedxml/#python-xml-libraries +# https://docs.python.org/3.10/library/xml.html#xml-vulnerabilities + +import pathlib +from flask import Flask +import threading +import multiprocessing +import time +from io import StringIO +import pytest + +HOST = "localhost" +PORT = 8080 + + +FLAG_PATH = pathlib.Path(__file__).with_name("flag") + +# ============================================================================== +# xml samples + +ok_xml = f""" +hello world +""" + +local_xxe = f""" + +]> +&xxe; +""" + +remote_xxe = f""" + +]> +&remote_xxe; +""" + +billion_laughs = """ + + + + + + + + + + + +]> +&lol9;""" + +quadratic_blowup = f""" + +]> +{"&oops;"*20000}""" + +dtd_retrieval = f""" + +bar +""" + +# ============================================================================== +# other setup + +# we set up local Flask application so we can tests whether loading external resources +# works (such as SSRF from DTD-retrival works) +app = Flask(__name__) + +@app.route("/alive") +def alive(): + return "ok" + +hit_dtd = False +@app.route("/test.dtd") +def test_dtd(): + global hit_dtd + hit_dtd = True + return """""" + +hit_xxe = False +@app.route("/xxe") +def test_xxe(): + global hit_xxe + hit_xxe = True + return "ok" + +def run_app(): + app.run(host=HOST, port=PORT) + +@pytest.fixture(scope="session", autouse=True) +def flask_app_running(): + # run flask in other thread + flask_thread = threading.Thread(target=run_app, daemon=True) + flask_thread.start() + + # give flask a bit of time to start + time.sleep(0.1) + + # ensure that the server works + import requests + requests.get(f"http://{HOST}:{PORT}/alive") + + yield + +def expects_timeout(func): + def inner(): + proc = multiprocessing.Process(target=func) + proc.start() + time.sleep(0.1) + assert proc.exitcode == None + proc.kill() + proc.join() + return inner + + +class TestExpectsTimeout: + "test that expects_timeout works as expected" + + @staticmethod + @expects_timeout + def test_slow(): + time.sleep(1000) + + @staticmethod + def test_fast(): + @expects_timeout + def fast_func(): + return "done!" + + with pytest.raises(AssertionError): + fast_func() + +# ============================================================================== +import xml.sax +import xml.sax.handler + +class SimpleHandler(xml.sax.ContentHandler): + def __init__(self): + self.result = [] + + def characters(self, data): + self.result.append(data) + +class TestSax(): + # always vuln to billion laughs, quadratic + + @staticmethod + @expects_timeout + def test_billion_laughs_allowed_by_default(): + parser = xml.sax.make_parser() + parser.parse(StringIO(billion_laughs)) + + @staticmethod + @expects_timeout + def test_quardratic_blowup_allowed_by_default(): + parser = xml.sax.make_parser() + parser.parse(StringIO(quadratic_blowup)) + + @staticmethod + def test_ok_xml(): + handler = SimpleHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(handler) + parser.parse(StringIO(ok_xml)) + assert handler.result == ["hello world"], handler.result + + @staticmethod + def test_xxe_disabled_by_default(): + handler = SimpleHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(handler) + parser.parse(StringIO(local_xxe)) + assert handler.result == [], handler.result + + @staticmethod + def test_local_xxe_manually_enabled(): + handler = SimpleHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(handler) + parser.setFeature(xml.sax.handler.feature_external_ges, True) + parser.parse(StringIO(local_xxe)) + assert handler.result[0] == "SECRET_FLAG", handler.result + + @staticmethod + def test_remote_xxe_manually_enabled(): + global hit_xxe + hit_xxe = False + + handler = SimpleHandler() + parser = xml.sax.make_parser() + parser.setContentHandler(handler) + parser.setFeature(xml.sax.handler.feature_external_ges, True) + parser.parse(StringIO(remote_xxe)) + assert handler.result == ["ok"], handler.result + assert hit_xxe == True + + @staticmethod + def test_dtd_disabled_by_default(): + global hit_dtd + hit_dtd = False + + parser = xml.sax.make_parser() + parser.parse(StringIO(dtd_retrieval)) + assert hit_dtd == False + + @staticmethod + def test_dtd_manually_enabled(): + global hit_dtd + hit_dtd = False + + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + parser.parse(StringIO(dtd_retrieval)) + assert hit_dtd == True + + +# ============================================================================== +import xml.etree.ElementTree + +class TestEtree: + + # always vuln to billion laughs, quadratic + @staticmethod + @expects_timeout + def test_billion_laughs_allowed_by_default(): + parser = xml.etree.ElementTree.XMLParser() + _root = xml.etree.ElementTree.fromstring(billion_laughs, parser=parser) + + @staticmethod + @expects_timeout + def test_quardratic_blowup_allowed_by_default(): + parser = xml.etree.ElementTree.XMLParser() + _root = xml.etree.ElementTree.fromstring(quadratic_blowup, parser=parser) + + @staticmethod + def test_ok_xml(): + parser = xml.etree.ElementTree.XMLParser() + root = xml.etree.ElementTree.fromstring(ok_xml, parser=parser) + assert root.tag == "test" + assert root.text == "hello world" + + @staticmethod + def test_ok_xml_sax_parser(): + # you _can_ pass a SAX parser to xml.etree... but it doesn't give you the output :| + parser = xml.sax.make_parser() + root = xml.etree.ElementTree.fromstring(ok_xml, parser=parser) + assert root == None + + @staticmethod + def test_ok_xml_lxml_parser(): + # this is technically possible, since parsers follow the same API, and the + # `fromstring` function is just a thin wrapper... seems very unlikely that + # anyone would do this though :| + parser = lxml.etree.XMLParser() + root = xml.etree.ElementTree.fromstring(ok_xml, parser=parser) + assert root.tag == "test" + assert root.text == "hello world" + + @staticmethod + def test_xxe_not_possible(): + parser = xml.etree.ElementTree.XMLParser() + try: + _root = xml.etree.ElementTree.fromstring(local_xxe, parser=parser) + assert False + except xml.etree.ElementTree.ParseError as e: + assert "undefined entity &xxe" in str(e) + + @staticmethod + def test_dtd_not_possible(): + global hit_dtd + hit_dtd = False + + parser = xml.etree.ElementTree.XMLParser() + _root = xml.etree.ElementTree.fromstring(dtd_retrieval, parser=parser) + assert hit_dtd == False + +# ============================================================================== +import lxml.etree + +class TestLxml: + # see https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser + @staticmethod + def test_billion_laughs_disabled_by_default(): + parser = lxml.etree.XMLParser() + try: + _root = lxml.etree.fromstring(billion_laughs, parser=parser) + assert False + except lxml.etree.XMLSyntaxError as e: + assert "Detected an entity reference loop" in str(e) + + @staticmethod + def test_quardratic_blowup_disabled_by_default(): + parser = lxml.etree.XMLParser() + try: + _root = lxml.etree.fromstring(quadratic_blowup, parser=parser) + assert False + except lxml.etree.XMLSyntaxError as e: + assert "Detected an entity reference loop" in str(e) + + @staticmethod + @expects_timeout + def test_billion_laughs_manually_enabled(): + parser = lxml.etree.XMLParser(huge_tree=True) + root = lxml.etree.fromstring(billion_laughs, parser=parser) + + @staticmethod + @expects_timeout + def test_quadratic_blowup_manually_enabled(): + parser = lxml.etree.XMLParser(huge_tree=True) + root = lxml.etree.fromstring(quadratic_blowup, parser=parser) + + @staticmethod + def test_billion_laughs_huge_tree_not_enough(): + parser = lxml.etree.XMLParser(huge_tree=True, resolve_entities=False) + root = lxml.etree.fromstring(billion_laughs, parser=parser) + assert root.tag == "lolz" + assert root.text == None + + @staticmethod + def test_quadratic_blowup_huge_tree_not_enough(): + parser = lxml.etree.XMLParser(huge_tree=True, resolve_entities=False) + root = lxml.etree.fromstring(quadratic_blowup, parser=parser) + assert root.tag == "foo" + assert root.text == None + + @staticmethod + def test_ok_xml(): + parser = lxml.etree.XMLParser() + root = lxml.etree.fromstring(ok_xml, parser=parser) + assert root.tag == "test" + assert root.text == "hello world" + + @staticmethod + def test_local_xxe_enabled_by_default(): + parser = lxml.etree.XMLParser() + root = lxml.etree.fromstring(local_xxe, parser=parser) + assert root.tag == "test" + assert root.text == "SECRET_FLAG\n", root.text + + @staticmethod + def test_local_xxe_disabled(): + parser = lxml.etree.XMLParser(resolve_entities=False) + root = lxml.etree.fromstring(local_xxe, parser=parser) + assert root.tag == "test" + assert root.text == None + + @staticmethod + def test_remote_xxe_disabled_by_default(): + global hit_xxe + hit_xxe = False + + parser = lxml.etree.XMLParser() + try: + root = lxml.etree.fromstring(remote_xxe, parser=parser) + assert False + except lxml.etree.XMLSyntaxError as e: + assert "Failure to process entity remote_xxe" in str(e) + assert hit_xxe == False + + @staticmethod + def test_remote_xxe_manually_enabled(): + global hit_xxe + hit_xxe = False + + parser = lxml.etree.XMLParser(no_network=False) + root = lxml.etree.fromstring(remote_xxe, parser=parser) + assert root.tag == "test" + assert root.text == "ok" + assert hit_xxe == True + + @staticmethod + def test_dtd_disabled_by_default(): + global hit_dtd + hit_dtd = False + + parser = lxml.etree.XMLParser() + root = lxml.etree.fromstring(dtd_retrieval, parser=parser) + assert hit_dtd == False + + @staticmethod + def test_dtd_manually_enabled(): + global hit_dtd + hit_dtd = False + + # Need to set BOTH load_dtd and no_network + parser = lxml.etree.XMLParser(load_dtd=True) + root = lxml.etree.fromstring(dtd_retrieval, parser=parser) + assert hit_dtd == False + + parser = lxml.etree.XMLParser(no_network=False) + root = lxml.etree.fromstring(dtd_retrieval, parser=parser) + assert hit_dtd == False + + parser = lxml.etree.XMLParser(load_dtd=True, no_network=False) + root = lxml.etree.fromstring(dtd_retrieval, parser=parser) + assert hit_dtd == True + + hit_dtd = False + + # Setting dtd_validation also does not allow the remote access + parser = lxml.etree.XMLParser(dtd_validation=True, load_dtd=True) + try: + root = lxml.etree.fromstring(dtd_retrieval, parser=parser) + except lxml.etree.XMLSyntaxError: + pass + assert hit_dtd == False + + +# ============================================================================== + +import xmltodict + +class TestXmltodict: + @staticmethod + def test_billion_laughs_disabled_by_default(): + d = xmltodict.parse(billion_laughs) + assert d == {"lolz": None}, d + + @staticmethod + def test_quardratic_blowup_disabled_by_default(): + d = xmltodict.parse(quadratic_blowup) + assert d == {"foo": None}, d + + @staticmethod + @expects_timeout + def test_billion_laughs_manually_enabled(): + xmltodict.parse(billion_laughs, disable_entities=False) + + @staticmethod + @expects_timeout + def test_quardratic_blowup_manually_enabled(): + xmltodict.parse(quadratic_blowup, disable_entities=False) + + @staticmethod + def test_ok_xml(): + d = xmltodict.parse(ok_xml) + assert d == {"test": "hello world"}, d + + @staticmethod + def test_local_xxe_not_possible(): + d = xmltodict.parse(local_xxe) + assert d == {"test": None} + + d = xmltodict.parse(local_xxe, disable_entities=False) + assert d == {"test": None} + + @staticmethod + def test_remote_xxe_not_possible(): + global hit_xxe + hit_xxe = False + + d = xmltodict.parse(remote_xxe) + assert d == {"test": None} + assert hit_xxe == False + + d = xmltodict.parse(remote_xxe, disable_entities=False) + assert d == {"test": None} + assert hit_xxe == False + + @staticmethod + def test_dtd_not_possible(): + global hit_dtd + hit_dtd = False + + d = xmltodict.parse(dtd_retrieval) + assert hit_dtd == False + +# ============================================================================== +import xml.dom.minidom + +class TestMinidom: + @staticmethod + @expects_timeout + def test_billion_laughs(): + xml.dom.minidom.parseString(billion_laughs) + + @staticmethod + @expects_timeout + def test_quardratic_blowup(): + xml.dom.minidom.parseString(quadratic_blowup) + + @staticmethod + def test_ok_xml(): + doc = xml.dom.minidom.parseString(ok_xml) + assert doc.documentElement.tagName == "test" + assert doc.documentElement.childNodes[0].data == "hello world" + + @staticmethod + def test_xxe(): + # disabled by default + doc = xml.dom.minidom.parseString(local_xxe) + assert doc.documentElement.tagName == "test" + assert doc.documentElement.childNodes == [] + + # but can be turned on + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + doc = xml.dom.minidom.parseString(local_xxe, parser=parser) + assert doc.documentElement.tagName == "test" + assert doc.documentElement.childNodes[0].data == "SECRET_FLAG" + + # which also works remotely + global hit_xxe + hit_xxe = False + + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + _doc = xml.dom.minidom.parseString(remote_xxe, parser=parser) + assert hit_xxe == True + + @staticmethod + def test_dtd(): + # not possible by default + global hit_dtd + hit_dtd = False + + _doc = xml.dom.minidom.parseString(dtd_retrieval) + assert hit_dtd == False + + # but can be turned on + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + _doc = xml.dom.minidom.parseString(dtd_retrieval, parser=parser) + assert hit_dtd == True + +# ============================================================================== +import xml.dom.pulldom + +class TestPulldom: + @staticmethod + @expects_timeout + def test_billion_laughs(): + doc = xml.dom.pulldom.parseString(billion_laughs) + # you NEED to iterate over the items for it to take long + for event, node in doc: + pass + + @staticmethod + @expects_timeout + def test_quardratic_blowup(): + doc = xml.dom.pulldom.parseString(quadratic_blowup) + for event, node in doc: + pass + + @staticmethod + def test_ok_xml(): + doc = xml.dom.pulldom.parseString(ok_xml) + for event, node in doc: + if event == xml.dom.pulldom.START_ELEMENT: + assert node.tagName == "test" + elif event == xml.dom.pulldom.CHARACTERS: + assert node.data == "hello world" + + @staticmethod + def test_xxe(): + # disabled by default + doc = xml.dom.pulldom.parseString(local_xxe) + found_flag = False + for event, node in doc: + if event == xml.dom.pulldom.START_ELEMENT: + assert node.tagName == "test" + elif event == xml.dom.pulldom.CHARACTERS: + if node.data == "SECRET_FLAG": + found_flag = True + assert found_flag == False + + # but can be turned on + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + doc = xml.dom.pulldom.parseString(local_xxe, parser=parser) + found_flag = False + for event, node in doc: + if event == xml.dom.pulldom.START_ELEMENT: + assert node.tagName == "test" + elif event == xml.dom.pulldom.CHARACTERS: + if node.data == "SECRET_FLAG": + found_flag = True + assert found_flag == True + + # which also works remotely + global hit_xxe + hit_xxe = False + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + doc = xml.dom.pulldom.parseString(remote_xxe, parser=parser) + assert hit_xxe == False + for event, node in doc: + pass + assert hit_xxe == True + + @staticmethod + def test_dtd(): + # not possible by default + global hit_dtd + hit_dtd = False + + doc = xml.dom.pulldom.parseString(dtd_retrieval) + for event, node in doc: + pass + assert hit_dtd == False + + # but can be turned on + parser = xml.sax.make_parser() + parser.setFeature(xml.sax.handler.feature_external_ges, True) + doc = xml.dom.pulldom.parseString(dtd_retrieval, parser=parser) + for event, node in doc: + pass + assert hit_dtd == True + +# ============================================================================== +import xml.parsers.expat + +class TestExpat: + # this is the underlying parser implementation used by the rest of the Python + # standard library. But people are probably not using this directly. + + @staticmethod + @expects_timeout + def test_billion_laughs(): + parser = xml.parsers.expat.ParserCreate() + parser.Parse(billion_laughs, True) + + @staticmethod + @expects_timeout + def test_quardratic_blowup(): + parser = xml.parsers.expat.ParserCreate() + parser.Parse(quadratic_blowup, True) + + @staticmethod + def test_ok_xml(): + char_data_recv = [] + def char_data_handler(data): + char_data_recv.append(data) + + parser = xml.parsers.expat.ParserCreate() + parser.CharacterDataHandler = char_data_handler + parser.Parse(ok_xml, True) + + assert char_data_recv == ["hello world"] + + @staticmethod + def test_xxe(): + # not vuln by default + char_data_recv = [] + def char_data_handler(data): + char_data_recv.append(data) + + parser = xml.parsers.expat.ParserCreate() + parser.CharacterDataHandler = char_data_handler + parser.Parse(local_xxe, True) + + assert char_data_recv == [] + + # there might be ways to make it vuln, but I did not investigate futher. + + @staticmethod + def test_dtd(): + # not vuln by default + global hit_dtd + hit_dtd = False + + parser = xml.parsers.expat.ParserCreate() + parser.Parse(dtd_retrieval, True) + assert hit_dtd == False + + # there might be ways to make it vuln, but I did not investigate futher. diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/poc/flag b/python/ql/test/experimental/library-tests/frameworks/XML/poc/flag new file mode 100644 index 000000000000..45c9436ee9f2 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/poc/flag @@ -0,0 +1 @@ +SECRET_FLAG diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/poc/this-dir-is-not-extracted b/python/ql/test/experimental/library-tests/frameworks/XML/poc/this-dir-is-not-extracted new file mode 100644 index 000000000000..b1925ade1d3a --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/poc/this-dir-is-not-extracted @@ -0,0 +1 @@ +just FYI diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/xml_dom.py b/python/ql/test/experimental/library-tests/frameworks/XML/xml_dom.py new file mode 100644 index 000000000000..7dce29fc7b9d --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/xml_dom.py @@ -0,0 +1,31 @@ +from io import StringIO +import xml.dom.minidom +import xml.dom.pulldom +import xml.sax + +x = "some xml" + +# minidom +xml.dom.minidom.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.dom.minidom.parse(file=StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.dom.minidom.parseString(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.dom.minidom.parseString(string=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + + +# pulldom +xml.dom.pulldom.parse(StringIO(x))['START_DOCUMENT'][1] # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.dom.pulldom.parse(stream_or_string=StringIO(x))['START_DOCUMENT'][1] # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.dom.pulldom.parseString(x)['START_DOCUMENT'][1] # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.dom.pulldom.parseString(string=x)['START_DOCUMENT'][1] # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + + +# These are based on SAX parses, and you can specify your own, so you can expose yourself to XXE (yay/) +parser = xml.sax.make_parser() +parser.setFeature(xml.sax.handler.feature_external_ges, True) +xml.dom.minidom.parse(StringIO(x), parser) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' +xml.dom.minidom.parse(StringIO(x), parser=parser) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' + +xml.dom.pulldom.parse(StringIO(x), parser) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' +xml.dom.pulldom.parse(StringIO(x), parser=parser) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/xml_etree.py b/python/ql/test/experimental/library-tests/frameworks/XML/xml_etree.py new file mode 100644 index 000000000000..df126e458e2d --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/xml_etree.py @@ -0,0 +1,45 @@ +from io import StringIO +import xml.etree.ElementTree + +x = "some xml" + +# Parsing in different ways +xml.etree.ElementTree.fromstring(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.etree.ElementTree.fromstring(text=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.etree.ElementTree.fromstringlist([x]) # $ input=List vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.etree.ElementTree.fromstringlist(sequence=[x]) # $ input=List vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.etree.ElementTree.XML(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.etree.ElementTree.XML(text=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.etree.ElementTree.XMLID(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.etree.ElementTree.XMLID(text=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.etree.ElementTree.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.etree.ElementTree.parse(source=StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.etree.ElementTree.iterparse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.etree.ElementTree.iterparse(source=StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + + +# With parsers (no options available to disable/enable security features) +parser = xml.etree.ElementTree.XMLParser() +xml.etree.ElementTree.fromstring(x, parser=parser) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + +# manual use of feed method +parser = xml.etree.ElementTree.XMLParser() +parser.feed(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +parser.feed(data=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +parser.close() + +# manual use of feed method on XMLPullParser +parser = xml.etree.ElementTree.XMLPullParser() +parser.feed(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +parser.feed(data=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +parser.close() + +# note: it's technically possible to use the thing wrapper func `fromstring` with an +# `lxml` parser, and thereby change what vulnerabilities you are exposed to.. but it +# seems very unlikely that anyone would do this, so we have intentionally not added any +# tests for this. diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/xml_sax.py b/python/ql/test/experimental/library-tests/frameworks/XML/xml_sax.py new file mode 100644 index 000000000000..158e62ffae6b --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/xml_sax.py @@ -0,0 +1,64 @@ +from io import StringIO +import xml.sax + +x = "some xml" + +class MainHandler(xml.sax.ContentHandler): + def __init__(self): + self._result = [] + + def characters(self, data): + self._result.append(data) + +xml.sax.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.sax.parse(source=StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +xml.sax.parseString(x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' +xml.sax.parseString(string=x) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' + +parser = xml.sax.make_parser() +parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' +parser.parse(source=StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +# You can make it vuln to both XXE and DTD retrieval by setting this flag +# see https://docs.python.org/3/library/xml.sax.handler.html#xml.sax.handler.feature_external_ges +parser = xml.sax.make_parser() +parser.setFeature(xml.sax.handler.feature_external_ges, True) +parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' + +parser = xml.sax.make_parser() +parser.setFeature(xml.sax.handler.feature_external_ges, False) +parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +# Forward Type Tracking test +def func(cond): + parser = xml.sax.make_parser() + if cond: + parser.setFeature(xml.sax.handler.feature_external_ges, True) + parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' + else: + parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +# make it vuln, then making it safe +# a bit of an edge-case, but is nice to be able to handle. +parser = xml.sax.make_parser() +parser.setFeature(xml.sax.handler.feature_external_ges, True) +parser.setFeature(xml.sax.handler.feature_external_ges, False) +parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='Quadratic Blowup' + +def check_conditional_assignment(cond): + parser = xml.sax.make_parser() + if cond: + parser.setFeature(xml.sax.handler.feature_external_ges, True) + else: + parser.setFeature(xml.sax.handler.feature_external_ges, False) + parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' + +def check_conditional_assignment2(cond): + parser = xml.sax.make_parser() + if cond: + flag_value = True + else: + flag_value = False + parser.setFeature(xml.sax.handler.feature_external_ges, flag_value) + parser.parse(StringIO(x)) # $ input=StringIO(..) vuln='Billion Laughs' vuln='DTD retrieval' vuln='Quadratic Blowup' vuln='XXE' diff --git a/python/ql/test/experimental/library-tests/frameworks/XML/xmltodict.py b/python/ql/test/experimental/library-tests/frameworks/XML/xmltodict.py new file mode 100644 index 000000000000..473e51c9fe66 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/XML/xmltodict.py @@ -0,0 +1,8 @@ +import xmltodict + +x = "some xml" + +xmltodict.parse(x) # $ input=x +xmltodict.parse(xml_input=x) # $ input=x + +xmltodict.parse(x, disable_entities=False) # $ input=x vuln='Billion Laughs' vuln='Quadratic Blowup' diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/SimpleXmlRpcServer.expected b/python/ql/test/experimental/query-tests/Security/CWE-611/SimpleXmlRpcServer.expected new file mode 100644 index 000000000000..4a08d61c47af --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-611/SimpleXmlRpcServer.expected @@ -0,0 +1 @@ +| xmlrpc_server.py:7:10:7:48 | ControlFlowNode for SimpleXMLRPCServer() | SimpleXMLRPCServer is vulnerable to: Billion Laughs, Quadratic Blowup. | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/SimpleXmlRpcServer.qlref b/python/ql/test/experimental/query-tests/Security/CWE-611/SimpleXmlRpcServer.qlref new file mode 100644 index 000000000000..a0b30e6d69b8 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-611/SimpleXmlRpcServer.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-611/SimpleXmlRpcServer.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/XmlEntityInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-611/XmlEntityInjection.expected index 081a8c6e6af8..25594b4ddaaf 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/XmlEntityInjection.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-611/XmlEntityInjection.expected @@ -1,243 +1,27 @@ edges -| lxml_etree.py:11:19:11:25 | ControlFlowNode for request | lxml_etree.py:11:19:11:30 | ControlFlowNode for Attribute | -| lxml_etree.py:11:19:11:30 | ControlFlowNode for Attribute | lxml_etree.py:11:19:11:45 | ControlFlowNode for Subscript | -| lxml_etree.py:11:19:11:45 | ControlFlowNode for Subscript | lxml_etree.py:13:34:13:44 | ControlFlowNode for xml_content | -| lxml_etree.py:17:19:17:25 | ControlFlowNode for request | lxml_etree.py:17:19:17:30 | ControlFlowNode for Attribute | -| lxml_etree.py:17:19:17:30 | ControlFlowNode for Attribute | lxml_etree.py:17:19:17:45 | ControlFlowNode for Subscript | -| lxml_etree.py:17:19:17:45 | ControlFlowNode for Subscript | lxml_etree.py:19:38:19:50 | ControlFlowNode for List | -| lxml_etree.py:23:19:23:25 | ControlFlowNode for request | lxml_etree.py:23:19:23:30 | ControlFlowNode for Attribute | -| lxml_etree.py:23:19:23:30 | ControlFlowNode for Attribute | lxml_etree.py:23:19:23:45 | ControlFlowNode for Subscript | -| lxml_etree.py:23:19:23:45 | ControlFlowNode for Subscript | lxml_etree.py:25:27:25:37 | ControlFlowNode for xml_content | -| lxml_etree.py:29:19:29:25 | ControlFlowNode for request | lxml_etree.py:29:19:29:30 | ControlFlowNode for Attribute | -| lxml_etree.py:29:19:29:30 | ControlFlowNode for Attribute | lxml_etree.py:29:19:29:45 | ControlFlowNode for Subscript | -| lxml_etree.py:29:19:29:45 | ControlFlowNode for Subscript | lxml_etree.py:31:29:31:49 | ControlFlowNode for StringIO() | -| lxml_etree.py:37:19:37:25 | ControlFlowNode for request | lxml_etree.py:37:19:37:30 | ControlFlowNode for Attribute | -| lxml_etree.py:37:19:37:30 | ControlFlowNode for Attribute | lxml_etree.py:37:19:37:45 | ControlFlowNode for Subscript | -| lxml_etree.py:37:19:37:45 | ControlFlowNode for Subscript | lxml_etree.py:40:34:40:44 | ControlFlowNode for xml_content | -| lxml_etree.py:44:19:44:25 | ControlFlowNode for request | lxml_etree.py:44:19:44:30 | ControlFlowNode for Attribute | -| lxml_etree.py:44:19:44:30 | ControlFlowNode for Attribute | lxml_etree.py:44:19:44:45 | ControlFlowNode for Subscript | -| lxml_etree.py:44:19:44:45 | ControlFlowNode for Subscript | lxml_etree.py:47:34:47:44 | ControlFlowNode for xml_content | -| lxml_etree.py:54:19:54:25 | ControlFlowNode for request | lxml_etree.py:54:19:54:30 | ControlFlowNode for Attribute | -| lxml_etree.py:54:19:54:30 | ControlFlowNode for Attribute | lxml_etree.py:54:19:54:45 | ControlFlowNode for Subscript | -| lxml_etree.py:54:19:54:45 | ControlFlowNode for Subscript | lxml_etree.py:57:34:57:44 | ControlFlowNode for xml_content | -| lxml_etree.py:65:19:65:25 | ControlFlowNode for request | lxml_etree.py:65:19:65:30 | ControlFlowNode for Attribute | -| lxml_etree.py:65:19:65:30 | ControlFlowNode for Attribute | lxml_etree.py:65:19:65:45 | ControlFlowNode for Subscript | -| lxml_etree.py:65:19:65:45 | ControlFlowNode for Subscript | lxml_etree.py:68:34:68:44 | ControlFlowNode for xml_content | -| lxml_etree.py:73:19:73:25 | ControlFlowNode for request | lxml_etree.py:73:19:73:30 | ControlFlowNode for Attribute | -| lxml_etree.py:73:19:73:30 | ControlFlowNode for Attribute | lxml_etree.py:73:19:73:45 | ControlFlowNode for Subscript | -| lxml_etree.py:73:19:73:45 | ControlFlowNode for Subscript | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | -| xml_dom.py:13:19:13:25 | ControlFlowNode for request | xml_dom.py:13:19:13:30 | ControlFlowNode for Attribute | -| xml_dom.py:13:19:13:30 | ControlFlowNode for Attribute | xml_dom.py:13:19:13:45 | ControlFlowNode for Subscript | -| xml_dom.py:13:19:13:45 | ControlFlowNode for Subscript | xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | -| xml_dom.py:19:19:19:25 | ControlFlowNode for request | xml_dom.py:19:19:19:30 | ControlFlowNode for Attribute | -| xml_dom.py:19:19:19:30 | ControlFlowNode for Attribute | xml_dom.py:19:19:19:45 | ControlFlowNode for Subscript | -| xml_dom.py:19:19:19:45 | ControlFlowNode for Subscript | xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | -| xml_dom.py:25:19:25:25 | ControlFlowNode for request | xml_dom.py:25:19:25:30 | ControlFlowNode for Attribute | -| xml_dom.py:25:19:25:30 | ControlFlowNode for Attribute | xml_dom.py:25:19:25:45 | ControlFlowNode for Subscript | -| xml_dom.py:25:19:25:45 | ControlFlowNode for Subscript | xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | -| xml_dom.py:31:19:31:25 | ControlFlowNode for request | xml_dom.py:31:19:31:30 | ControlFlowNode for Attribute | -| xml_dom.py:31:19:31:30 | ControlFlowNode for Attribute | xml_dom.py:31:19:31:45 | ControlFlowNode for Subscript | -| xml_dom.py:31:19:31:45 | ControlFlowNode for Subscript | xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | -| xml_dom.py:39:19:39:25 | ControlFlowNode for request | xml_dom.py:39:19:39:30 | ControlFlowNode for Attribute | -| xml_dom.py:39:19:39:30 | ControlFlowNode for Attribute | xml_dom.py:39:19:39:45 | ControlFlowNode for Subscript | -| xml_dom.py:39:19:39:45 | ControlFlowNode for Subscript | xml_dom.py:43:34:43:54 | ControlFlowNode for StringIO() | -| xml_etree.py:13:19:13:25 | ControlFlowNode for request | xml_etree.py:13:19:13:30 | ControlFlowNode for Attribute | -| xml_etree.py:13:19:13:30 | ControlFlowNode for Attribute | xml_etree.py:13:19:13:45 | ControlFlowNode for Subscript | -| xml_etree.py:13:19:13:45 | ControlFlowNode for Subscript | xml_etree.py:15:45:15:55 | ControlFlowNode for xml_content | -| xml_etree.py:19:19:19:25 | ControlFlowNode for request | xml_etree.py:19:19:19:30 | ControlFlowNode for Attribute | -| xml_etree.py:19:19:19:30 | ControlFlowNode for Attribute | xml_etree.py:19:19:19:45 | ControlFlowNode for Subscript | -| xml_etree.py:19:19:19:45 | ControlFlowNode for Subscript | xml_etree.py:21:49:21:59 | ControlFlowNode for xml_content | -| xml_etree.py:25:19:25:25 | ControlFlowNode for request | xml_etree.py:25:19:25:30 | ControlFlowNode for Attribute | -| xml_etree.py:25:19:25:30 | ControlFlowNode for Attribute | xml_etree.py:25:19:25:45 | ControlFlowNode for Subscript | -| xml_etree.py:25:19:25:45 | ControlFlowNode for Subscript | xml_etree.py:27:38:27:48 | ControlFlowNode for xml_content | -| xml_etree.py:31:19:31:25 | ControlFlowNode for request | xml_etree.py:31:19:31:30 | ControlFlowNode for Attribute | -| xml_etree.py:31:19:31:30 | ControlFlowNode for Attribute | xml_etree.py:31:19:31:45 | ControlFlowNode for Subscript | -| xml_etree.py:31:19:31:45 | ControlFlowNode for Subscript | xml_etree.py:33:40:33:60 | ControlFlowNode for StringIO() | -| xml_etree.py:39:19:39:25 | ControlFlowNode for request | xml_etree.py:39:19:39:30 | ControlFlowNode for Attribute | -| xml_etree.py:39:19:39:30 | ControlFlowNode for Attribute | xml_etree.py:39:19:39:45 | ControlFlowNode for Subscript | -| xml_etree.py:39:19:39:45 | ControlFlowNode for Subscript | xml_etree.py:42:45:42:55 | ControlFlowNode for xml_content | -| xml_etree.py:46:19:46:25 | ControlFlowNode for request | xml_etree.py:46:19:46:30 | ControlFlowNode for Attribute | -| xml_etree.py:46:19:46:30 | ControlFlowNode for Attribute | xml_etree.py:46:19:46:45 | ControlFlowNode for Subscript | -| xml_etree.py:46:19:46:45 | ControlFlowNode for Subscript | xml_etree.py:49:45:49:55 | ControlFlowNode for xml_content | -| xml_etree.py:53:19:53:25 | ControlFlowNode for request | xml_etree.py:53:19:53:30 | ControlFlowNode for Attribute | -| xml_etree.py:53:19:53:30 | ControlFlowNode for Attribute | xml_etree.py:53:19:53:45 | ControlFlowNode for Subscript | -| xml_etree.py:53:19:53:45 | ControlFlowNode for Subscript | xml_etree.py:56:45:56:55 | ControlFlowNode for xml_content | -| xml_etree.py:60:19:60:25 | ControlFlowNode for request | xml_etree.py:60:19:60:30 | ControlFlowNode for Attribute | -| xml_etree.py:60:19:60:30 | ControlFlowNode for Attribute | xml_etree.py:60:19:60:45 | ControlFlowNode for Subscript | -| xml_etree.py:60:19:60:45 | ControlFlowNode for Subscript | xml_etree.py:64:45:64:55 | ControlFlowNode for xml_content | -| xml_sax_make_parser.py:31:19:31:25 | ControlFlowNode for request | xml_sax_make_parser.py:31:19:31:30 | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:31:19:31:30 | ControlFlowNode for Attribute | xml_sax_make_parser.py:31:19:31:45 | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:31:19:31:45 | ControlFlowNode for Subscript | xml_sax_make_parser.py:36:18:36:38 | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:42:19:42:25 | ControlFlowNode for request | xml_sax_make_parser.py:42:19:42:30 | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:42:19:42:30 | ControlFlowNode for Attribute | xml_sax_make_parser.py:42:19:42:45 | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:42:19:42:45 | ControlFlowNode for Subscript | xml_sax_make_parser.py:49:18:49:38 | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | xml_sax_make_parser.py:57:19:57:30 | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:57:19:57:30 | ControlFlowNode for Attribute | xml_sax_make_parser.py:57:19:57:45 | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:57:19:57:45 | ControlFlowNode for Subscript | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:69:19:69:25 | ControlFlowNode for request | xml_sax_make_parser.py:69:19:69:30 | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:69:19:69:30 | ControlFlowNode for Attribute | xml_sax_make_parser.py:69:19:69:45 | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:69:19:69:45 | ControlFlowNode for Subscript | xml_sax_make_parser.py:73:34:73:54 | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:79:19:79:25 | ControlFlowNode for request | xml_sax_make_parser.py:79:19:79:30 | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:79:19:79:30 | ControlFlowNode for Attribute | xml_sax_make_parser.py:79:19:79:45 | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:79:19:79:45 | ControlFlowNode for Subscript | xml_sax_make_parser.py:86:22:86:42 | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | xml_sax_make_parser.py:91:19:91:30 | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:91:19:91:30 | ControlFlowNode for Attribute | xml_sax_make_parser.py:91:19:91:45 | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:91:19:91:45 | ControlFlowNode for Subscript | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | -| xml_to_dict.py:9:19:9:25 | ControlFlowNode for request | xml_to_dict.py:9:19:9:30 | ControlFlowNode for Attribute | -| xml_to_dict.py:9:19:9:30 | ControlFlowNode for Attribute | xml_to_dict.py:9:19:9:45 | ControlFlowNode for Subscript | -| xml_to_dict.py:9:19:9:45 | ControlFlowNode for Subscript | xml_to_dict.py:11:28:11:38 | ControlFlowNode for xml_content | -| xml_to_dict.py:15:19:15:25 | ControlFlowNode for request | xml_to_dict.py:15:19:15:30 | ControlFlowNode for Attribute | -| xml_to_dict.py:15:19:15:30 | ControlFlowNode for Attribute | xml_to_dict.py:15:19:15:45 | ControlFlowNode for Subscript | -| xml_to_dict.py:15:19:15:45 | ControlFlowNode for Subscript | xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | +| test.py:8:19:8:25 | ControlFlowNode for request | test.py:8:19:8:30 | ControlFlowNode for Attribute | +| test.py:8:19:8:30 | ControlFlowNode for Attribute | test.py:8:19:8:45 | ControlFlowNode for Subscript | +| test.py:8:19:8:45 | ControlFlowNode for Subscript | test.py:9:34:9:44 | ControlFlowNode for xml_content | +| test.py:13:19:13:25 | ControlFlowNode for request | test.py:13:19:13:30 | ControlFlowNode for Attribute | +| test.py:13:19:13:30 | ControlFlowNode for Attribute | test.py:13:19:13:45 | ControlFlowNode for Subscript | +| test.py:13:19:13:45 | ControlFlowNode for Subscript | test.py:15:34:15:44 | ControlFlowNode for xml_content | +| test.py:19:19:19:25 | ControlFlowNode for request | test.py:19:19:19:30 | ControlFlowNode for Attribute | +| test.py:19:19:19:30 | ControlFlowNode for Attribute | test.py:19:19:19:45 | ControlFlowNode for Subscript | +| test.py:19:19:19:45 | ControlFlowNode for Subscript | test.py:30:34:30:44 | ControlFlowNode for xml_content | nodes -| lxml_etree.py:11:19:11:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:11:19:11:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:11:19:11:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:13:34:13:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| lxml_etree.py:17:19:17:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:17:19:17:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:17:19:17:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:19:38:19:50 | ControlFlowNode for List | semmle.label | ControlFlowNode for List | -| lxml_etree.py:23:19:23:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:23:19:23:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:23:19:23:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:25:27:25:37 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| lxml_etree.py:29:19:29:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:29:19:29:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:29:19:29:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:31:29:31:49 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| lxml_etree.py:37:19:37:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:37:19:37:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:37:19:37:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:40:34:40:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| lxml_etree.py:44:19:44:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:44:19:44:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:44:19:44:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:47:34:47:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| lxml_etree.py:54:19:54:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:54:19:54:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:54:19:54:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:57:34:57:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| lxml_etree.py:65:19:65:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:65:19:65:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:65:19:65:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:68:34:68:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| lxml_etree.py:73:19:73:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| lxml_etree.py:73:19:73:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| lxml_etree.py:73:19:73:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_dom.py:13:19:13:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_dom.py:13:19:13:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_dom.py:13:19:13:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_dom.py:19:19:19:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_dom.py:19:19:19:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_dom.py:19:19:19:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_dom.py:25:19:25:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_dom.py:25:19:25:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_dom.py:25:19:25:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_dom.py:31:19:31:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_dom.py:31:19:31:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_dom.py:31:19:31:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_dom.py:39:19:39:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_dom.py:39:19:39:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_dom.py:39:19:39:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_dom.py:43:34:43:54 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_etree.py:13:19:13:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:13:19:13:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:13:19:13:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:15:45:15:55 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_etree.py:19:19:19:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:19:19:19:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:19:19:19:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:21:49:21:59 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_etree.py:25:19:25:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:25:19:25:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:25:19:25:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:27:38:27:48 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_etree.py:31:19:31:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:31:19:31:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:31:19:31:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:33:40:33:60 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_etree.py:39:19:39:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:39:19:39:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:39:19:39:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:42:45:42:55 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_etree.py:46:19:46:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:46:19:46:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:46:19:46:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:49:45:49:55 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_etree.py:53:19:53:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:53:19:53:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:53:19:53:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:56:45:56:55 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_etree.py:60:19:60:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_etree.py:60:19:60:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_etree.py:60:19:60:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_etree.py:64:45:64:55 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_sax_make_parser.py:31:19:31:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_sax_make_parser.py:31:19:31:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:31:19:31:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:36:18:36:38 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:42:19:42:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_sax_make_parser.py:42:19:42:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:42:19:42:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:49:18:49:38 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_sax_make_parser.py:57:19:57:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:57:19:57:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:69:19:69:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_sax_make_parser.py:69:19:69:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:69:19:69:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:73:34:73:54 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:79:19:79:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_sax_make_parser.py:79:19:79:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:79:19:79:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:86:22:86:42 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_sax_make_parser.py:91:19:91:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_sax_make_parser.py:91:19:91:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | semmle.label | ControlFlowNode for StringIO() | -| xml_to_dict.py:9:19:9:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_to_dict.py:9:19:9:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_to_dict.py:9:19:9:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_to_dict.py:11:28:11:38 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | -| xml_to_dict.py:15:19:15:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| xml_to_dict.py:15:19:15:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| xml_to_dict.py:15:19:15:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | +| test.py:8:19:8:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test.py:8:19:8:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| test.py:8:19:8:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| test.py:9:34:9:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | +| test.py:13:19:13:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test.py:13:19:13:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| test.py:13:19:13:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| test.py:15:34:15:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | +| test.py:19:19:19:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test.py:19:19:19:30 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| test.py:19:19:19:45 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| test.py:30:34:30:44 | ControlFlowNode for xml_content | semmle.label | ControlFlowNode for xml_content | subpaths #select -| lxml_etree.py:13:34:13:44 | ControlFlowNode for xml_content | lxml_etree.py:11:19:11:25 | ControlFlowNode for request | lxml_etree.py:13:34:13:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:13:34:13:44 | ControlFlowNode for xml_content | This | lxml_etree.py:11:19:11:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:19:38:19:50 | ControlFlowNode for List | lxml_etree.py:17:19:17:25 | ControlFlowNode for request | lxml_etree.py:19:38:19:50 | ControlFlowNode for List | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:19:38:19:50 | ControlFlowNode for List | This | lxml_etree.py:17:19:17:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:25:27:25:37 | ControlFlowNode for xml_content | lxml_etree.py:23:19:23:25 | ControlFlowNode for request | lxml_etree.py:25:27:25:37 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:25:27:25:37 | ControlFlowNode for xml_content | This | lxml_etree.py:23:19:23:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:31:29:31:49 | ControlFlowNode for StringIO() | lxml_etree.py:29:19:29:25 | ControlFlowNode for request | lxml_etree.py:31:29:31:49 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:31:29:31:49 | ControlFlowNode for StringIO() | This | lxml_etree.py:29:19:29:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:40:34:40:44 | ControlFlowNode for xml_content | lxml_etree.py:37:19:37:25 | ControlFlowNode for request | lxml_etree.py:40:34:40:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:40:34:40:44 | ControlFlowNode for xml_content | This | lxml_etree.py:37:19:37:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:47:34:47:44 | ControlFlowNode for xml_content | lxml_etree.py:44:19:44:25 | ControlFlowNode for request | lxml_etree.py:47:34:47:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:47:34:47:44 | ControlFlowNode for xml_content | This | lxml_etree.py:44:19:44:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | lxml_etree.py:73:19:73:25 | ControlFlowNode for request | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | This | lxml_etree.py:73:19:73:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | lxml_etree.py:73:19:73:25 | ControlFlowNode for request | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | This | lxml_etree.py:73:19:73:25 | ControlFlowNode for request | user-provided value | -| lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | lxml_etree.py:73:19:73:25 | ControlFlowNode for request | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | lxml_etree.py:76:34:76:44 | ControlFlowNode for xml_content | This | lxml_etree.py:73:19:73:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | xml_dom.py:13:19:13:25 | ControlFlowNode for request | xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | This | xml_dom.py:13:19:13:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | xml_dom.py:13:19:13:25 | ControlFlowNode for request | xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_dom.py:15:34:15:54 | ControlFlowNode for StringIO() | This | xml_dom.py:13:19:13:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | xml_dom.py:19:19:19:25 | ControlFlowNode for request | xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | This | xml_dom.py:19:19:19:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | xml_dom.py:19:19:19:25 | ControlFlowNode for request | xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_dom.py:21:40:21:50 | ControlFlowNode for xml_content | This | xml_dom.py:19:19:19:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | xml_dom.py:25:19:25:25 | ControlFlowNode for request | xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | This | xml_dom.py:25:19:25:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | xml_dom.py:25:19:25:25 | ControlFlowNode for request | xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_dom.py:27:34:27:54 | ControlFlowNode for StringIO() | This | xml_dom.py:25:19:25:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | xml_dom.py:31:19:31:25 | ControlFlowNode for request | xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | This | xml_dom.py:31:19:31:25 | ControlFlowNode for request | user-provided value | -| xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | xml_dom.py:31:19:31:25 | ControlFlowNode for request | xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_dom.py:33:40:33:50 | ControlFlowNode for xml_content | This | xml_dom.py:31:19:31:25 | ControlFlowNode for request | user-provided value | -| xml_etree.py:49:45:49:55 | ControlFlowNode for xml_content | xml_etree.py:46:19:46:25 | ControlFlowNode for request | xml_etree.py:49:45:49:55 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | xml_etree.py:49:45:49:55 | ControlFlowNode for xml_content | This | xml_etree.py:46:19:46:25 | ControlFlowNode for request | user-provided value | -| xml_etree.py:56:45:56:55 | ControlFlowNode for xml_content | xml_etree.py:53:19:53:25 | ControlFlowNode for request | xml_etree.py:56:45:56:55 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to XXE. | xml_etree.py:56:45:56:55 | ControlFlowNode for xml_content | This | xml_etree.py:53:19:53:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to DTD retrieval. | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to XXE. | xml_sax_make_parser.py:63:18:63:38 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:57:19:57:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to DTD retrieval. | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | user-provided value | -| xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | $@ XML input is constructed from a $@ and is vulnerable to XXE. | xml_sax_make_parser.py:98:22:98:42 | ControlFlowNode for StringIO() | This | xml_sax_make_parser.py:91:19:91:25 | ControlFlowNode for request | user-provided value | -| xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | xml_to_dict.py:15:19:15:25 | ControlFlowNode for request | xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Billion Laughs. | xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | This | xml_to_dict.py:15:19:15:25 | ControlFlowNode for request | user-provided value | -| xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | xml_to_dict.py:15:19:15:25 | ControlFlowNode for request | xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to Quadratic Blowup. | xml_to_dict.py:17:28:17:38 | ControlFlowNode for xml_content | This | xml_to_dict.py:15:19:15:25 | ControlFlowNode for request | user-provided value | +| test.py:9:34:9:44 | ControlFlowNode for xml_content | test.py:8:19:8:25 | ControlFlowNode for request | test.py:9:34:9:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to: XXE. | test.py:9:34:9:44 | ControlFlowNode for xml_content | This | test.py:8:19:8:25 | ControlFlowNode for request | user-provided value | +| test.py:30:34:30:44 | ControlFlowNode for xml_content | test.py:19:19:19:25 | ControlFlowNode for request | test.py:30:34:30:44 | ControlFlowNode for xml_content | $@ XML input is constructed from a $@ and is vulnerable to: Billion Laughs, DTD retrieval, Quadratic Blowup, XXE. | test.py:30:34:30:44 | ControlFlowNode for xml_content | This | test.py:19:19:19:25 | ControlFlowNode for request | user-provided value | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/lxml_etree.py b/python/ql/test/experimental/query-tests/Security/CWE-611/lxml_etree.py deleted file mode 100644 index 2c3c6f5f2ffc..000000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/lxml_etree.py +++ /dev/null @@ -1,76 +0,0 @@ -from flask import request, Flask -from io import StringIO, BytesIO -import lxml.etree - -app = Flask(__name__) - -# Parsing - -@app.route("/lxml_etree_fromstring") -def lxml_etree_fromstring(): - xml_content = request.args['xml_content'] - - return lxml.etree.fromstring(xml_content).text - -@app.route("/lxml_etree_fromstringlist") -def lxml_etree_fromstringlist(): - xml_content = request.args['xml_content'] - - return lxml.etree.fromstringlist([xml_content]).text - -@app.route("/lxml_etree_XML") -def lxml_etree_XML(): - xml_content = request.args['xml_content'] - - return lxml.etree.XML(xml_content).text - -@app.route("/lxml_etree_parse") -def lxml_etree_parse(): - xml_content = request.args['xml_content'] - - return lxml.etree.parse(StringIO(xml_content)).getroot().text - -# With parsers - Default - -@app.route("/lxml_etree_fromstring-lxml.etree.XMLParser") -def lxml_parser(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.XMLParser() - return lxml.etree.fromstring(xml_content, parser=parser).text - -@app.route("/lxml_etree_fromstring-lxml.etree.get_default_parser") -def lxml_parser(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.get_default_parser() - return lxml.etree.fromstring(xml_content, parser=parser).text - -# With parsers - With options - -# XXE-safe -@app.route("/lxml_etree_fromstring-lxml.etree.XMLParser+") -def lxml_parser(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.XMLParser(resolve_entities=False) - return lxml.etree.fromstring(xml_content, parser=parser).text - -# Billion laughs and quadratic blowup (huge_tree) - -## Good (huge_tree=True but resolve_entities=False) - -@app.route("/lxml_etree_fromstring-lxml.etree.XMLParser+") -def lxml_parser(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.XMLParser(resolve_entities=False, huge_tree=True) - return lxml.etree.fromstring(xml_content, parser=parser).text - -## Bad -@app.route("/lxml_etree_fromstring-lxml.etree.XMLParser+") -def lxml_parser(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.XMLParser(huge_tree=True) - return lxml.etree.fromstring(xml_content, parser=parser).text diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/test.py b/python/ql/test/experimental/query-tests/Security/CWE-611/test.py new file mode 100644 index 000000000000..d9181c4cf346 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-611/test.py @@ -0,0 +1,30 @@ +from flask import Flask, request +import lxml.etree + +app = Flask(__name__) + +@app.route("/vuln-handler") +def vuln_handler(): + xml_content = request.args['xml_content'] + return lxml.etree.fromstring(xml_content).text + +@app.route("/safe-handler") +def safe_handler(): + xml_content = request.args['xml_content'] + parser = lxml.etree.XMLParser(resolve_entities=False) + return lxml.etree.fromstring(xml_content, parser=parser).text + +@app.route("/super-vuln-handler") +def super_vuln_handler(): + xml_content = request.args['xml_content'] + parser = lxml.etree.XMLParser( + # allows XXE + resolve_entities=True, + # allows remote XXE + no_network=False, + # together with `no_network=False`, allows DTD-retrival + load_dtd=True, + # allows DoS attacks + huge_tree=True, + ) + return lxml.etree.fromstring(xml_content, parser=parser).text diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_dom.py b/python/ql/test/experimental/query-tests/Security/CWE-611/xml_dom.py deleted file mode 100644 index 428a2d645a1b..000000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_dom.py +++ /dev/null @@ -1,44 +0,0 @@ -from flask import request, Flask -from io import StringIO, BytesIO -import xml.dom.minidom -import xml.dom.pulldom -import xml.sax - -app = Flask(__name__) - -# Parsing - -@app.route("/xml_minidom_parse") -def xml_minidom_parse(): - xml_content = request.args['xml_content'] - - return xml.dom.minidom.parse(StringIO(xml_content)).documentElement.childNodes - -@app.route("/xml_minidom_parseString") -def xml_minidom_parseString(): - xml_content = request.args['xml_content'] - - return xml.dom.minidom.parseString(xml_content).documentElement.childNodes - -@app.route("/xml_pulldom_parse") -def xml_pulldom_parse(): - xml_content = request.args['xml_content'] - - return xml.dom.pulldom.parse(StringIO(xml_content))['START_DOCUMENT'][1].documentElement.childNodes - -@app.route("/xml_pulldom_parseString") -def xml_pulldom_parseString(): - xml_content = request.args['xml_content'] - - return xml.dom.pulldom.parseString(xml_content)['START_DOCUMENT'][1].documentElement.childNodes - -# With parsers - -@app.route("/xml_minidom_parse_xml_sax_make_parser") -def xml_minidom_parse_xml_sax_make_parser(): - 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 - diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_etree.py b/python/ql/test/experimental/query-tests/Security/CWE-611/xml_etree.py deleted file mode 100644 index b9c980045e2a..000000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_etree.py +++ /dev/null @@ -1,64 +0,0 @@ -from flask import request, Flask -from io import StringIO, BytesIO -import xml.etree -import xml.etree.ElementTree -import lxml.etree - -app = Flask(__name__) - -# Parsing - -@app.route("/xml_etree_fromstring") -def xml_etree_fromstring(): - xml_content = request.args['xml_content'] - - return xml.etree.ElementTree.fromstring(xml_content).text - -@app.route("/xml_etree_fromstringlist") -def xml_etree_fromstringlist(): - xml_content = request.args['xml_content'] - - return xml.etree.ElementTree.fromstringlist(xml_content).text - -@app.route("/xml_etree_XML") -def xml_etree_XML(): - xml_content = request.args['xml_content'] - - return xml.etree.ElementTree.XML(xml_content).text - -@app.route("/xml_etree_parse") -def xml_etree_parse(): - xml_content = request.args['xml_content'] - - return xml.etree.ElementTree.parse(StringIO(xml_content)).getroot().text - -# With parsers - -@app.route("/xml_etree_fromstring-xml_etree_XMLParser") -def xml_parser_1(): - xml_content = request.args['xml_content'] - - parser = xml.etree.ElementTree.XMLParser() - return xml.etree.ElementTree.fromstring(xml_content, parser=parser).text - -@app.route("/xml_etree_fromstring-lxml_etree_XMLParser") -def xml_parser_2(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.XMLParser() - return xml.etree.ElementTree.fromstring(xml_content, parser=parser).text - -@app.route("/xml_etree_fromstring-lxml_get_default_parser") -def xml_parser_3(): - xml_content = request.args['xml_content'] - - parser = lxml.etree.get_default_parser() - return xml.etree.ElementTree.fromstring(xml_content, parser=parser).text - -@app.route("/xml_etree_fromstring-lxml_get_default_parser") -def xml_parser_4(): - xml_content = request.args['xml_content'] - - parser = xml.sax.make_parser() - parser.setFeature(xml.sax.handler.feature_external_ges, True) - return xml.etree.ElementTree.fromstring(xml_content, parser=parser).text \ No newline at end of file diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_sax_make_parser.py b/python/ql/test/experimental/query-tests/Security/CWE-611/xml_sax_make_parser.py deleted file mode 100644 index 9f858d99ddd2..000000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_sax_make_parser.py +++ /dev/null @@ -1,99 +0,0 @@ -from flask import request, Flask -from io import StringIO -import xml.sax - -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 - -# Forward Type Tracking test - -@app.route("forward_tracking1") -def forward_tracking1(action): - xml_content = request.args['xml_content'] - - parser = xml.sax.make_parser() - if action == 'load-config': - parser.setFeature(xml.sax.handler.feature_external_ges, False) - parser.parse("/not-user-controlled/default_config.xml") - else: - parser.parse(StringIO(xml_content)) - return - -@app.route("forward_tracking2") -def forward_tracking2(action): - xml_content = request.args['xml_content'] - - parser = xml.sax.make_parser() - if action == 'load-config': - parser.setFeature(xml.sax.handler.feature_external_ges, True) - parser.parse("/not-user-controlled/default_config.xml") - else: - parser.parse(StringIO(xml_content)) - return diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_to_dict.py b/python/ql/test/experimental/query-tests/Security/CWE-611/xml_to_dict.py deleted file mode 100644 index 2b91a22e1a22..000000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/xml_to_dict.py +++ /dev/null @@ -1,17 +0,0 @@ -from flask import request, Flask -from io import StringIO, BytesIO -import xmltodict - -app = Flask(__name__) - -@app.route("/xmltodict.parse") -def xmltodict_parse(): - xml_content = request.args['xml_content'] - - return xmltodict.parse(xml_content) - -@app.route("/xmltodict.parse2") -def xmltodict_parse2(): - xml_content = request.args['xml_content'] - - return xmltodict.parse(xml_content, disable_entities=False) \ No newline at end of file diff --git a/python/ql/test/experimental/query-tests/Security/CWE-611/xmlrpc_server.py b/python/ql/test/experimental/query-tests/Security/CWE-611/xmlrpc_server.py index baa433c4a8ab..83c18b549b3d 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-611/xmlrpc_server.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-611/xmlrpc_server.py @@ -1,10 +1,12 @@ from xmlrpc.server import SimpleXMLRPCServer -def foo(n): - return n +def foo(n: str): + print("foo called with arg:", n, type(n)) + return "ok" server = SimpleXMLRPCServer(("127.0.0.1", 8000)) server.register_function(foo, "foo") server.serve_forever() -# billion_laughs -> curl 127.0.0.1:8000 --data-raw ']>foo&lol9;' +# normal: curl 127.0.0.1:8000 --data-raw 'foo42' +# billion_laughs: curl 127.0.0.1:8000 --data-raw ']>foo&lol9;'