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;'