mirror of
https://github.com/github/codeql.git
synced 2026-04-30 11:15:13 +02:00
Merge branch 'master' into python-3521-revived
This commit is contained in:
@@ -31,8 +31,8 @@ predicate calls_super(FunctionObject f) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if the given name is white-listed for some reason */
|
||||
predicate whitelisted(string name) {
|
||||
/** Holds if the given name is allowed for some reason */
|
||||
predicate allowed(string name) {
|
||||
/*
|
||||
* The standard library specifically recommends this :(
|
||||
* See https://docs.python.org/3/library/socketserver.html#asynchronous-mixins
|
||||
@@ -53,7 +53,7 @@ where
|
||||
not name.matches("\\_\\_%\\_\\_") and
|
||||
not calls_super(o1) and
|
||||
not does_nothing(o2) and
|
||||
not whitelisted(name) and
|
||||
not allowed(name) and
|
||||
not o1.overrides(o2) and
|
||||
not o2.overrides(o1) and
|
||||
not c.declaresAttribute(name)
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
import python
|
||||
|
||||
from ClassObject c
|
||||
where not c.isC() and not c.isContextManager() and exists(c.declaredAttribute("__del__"))
|
||||
from ClassValue c
|
||||
where not c.isBuiltin() and not c.isContextManager() and exists(c.declaredAttribute("__del__"))
|
||||
select c,
|
||||
"Class " + c.getName() +
|
||||
" implements __del__ (presumably to release some resource). Consider making it a context manager."
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* the arguments with which it is called, and if it were called, would be likely to cause an error.
|
||||
* @kind problem
|
||||
* @tags maintainability
|
||||
* @problem.severity error
|
||||
* @sub-severity low
|
||||
* @problem.severity recommendation
|
||||
* @sub-severity high
|
||||
* @precision high
|
||||
* @id py/inheritance/incorrect-overridden-signature
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ where
|
||||
count(int line |
|
||||
exists(DuplicateBlock d | d.sourceFile() = f |
|
||||
line in [d.sourceStartLine() .. d.sourceEndLine()] and
|
||||
not whitelistedLineForDuplication(f, line)
|
||||
not allowlistedLineForDuplication(f, line)
|
||||
)
|
||||
)
|
||||
select f, n order by n desc
|
||||
|
||||
@@ -20,7 +20,7 @@ where
|
||||
count(int line |
|
||||
exists(SimilarBlock d | d.sourceFile() = f |
|
||||
line in [d.sourceStartLine() .. d.sourceEndLine()] and
|
||||
not whitelistedLineForDuplication(f, line)
|
||||
not allowlistedLineForDuplication(f, line)
|
||||
)
|
||||
)
|
||||
select f, n order by n desc
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<p>
|
||||
|
||||
The second two examples show safe checks.
|
||||
In <code>safe1</code>, a white-list is used. Although fairly inflexible,
|
||||
In <code>safe1</code>, an allowlist is used. Although fairly inflexible,
|
||||
this is easy to get right and is most likely to be safe.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -21,16 +21,16 @@ def unsafe2(request):
|
||||
|
||||
|
||||
|
||||
#Simplest and safest approach is to use a white-list
|
||||
#Simplest and safest approach is to use an allowlist
|
||||
|
||||
@app.route('/some/path/good1')
|
||||
def safe1(request):
|
||||
whitelist = [
|
||||
allowlist = [
|
||||
"example.com/home",
|
||||
"example.com/login",
|
||||
]
|
||||
target = request.args.get('target', '')
|
||||
if target in whitelist:
|
||||
if target in allowlist:
|
||||
return redirect(target)
|
||||
|
||||
#More complex example allowing sub-domains.
|
||||
|
||||
@@ -26,7 +26,7 @@ Ideally, follow these rules:
|
||||
<li>Do not allow directory separators such as "/" or "\" (depending on the file system).</li>
|
||||
<li>Do not rely on simply replacing problematic sequences such as "../". For example, after
|
||||
applying this filter to ".../...//", the resulting string would still be "../".</li>
|
||||
<li>Use a whitelist of known good patterns.</li>
|
||||
<li>Use an allowlist of known good patterns.</li>
|
||||
</ul>
|
||||
</recommendation>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ safe before using it.</p>
|
||||
|
||||
<p>The following example shows two functions. The first is unsafe as it takes a shell script that can be changed
|
||||
by a user, and passes it straight to <code>subprocess.call()</code> without examining it first.
|
||||
The second is safe as it selects the command from a predefined white-list.</p>
|
||||
The second is safe as it selects the command from a predefined allowlist.</p>
|
||||
|
||||
<sample src="examples/command_injection.py" />
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ class CommandInjectionConfiguration extends TaintTracking::Configuration {
|
||||
|
||||
override predicate isExtension(TaintTracking::Extension extension) {
|
||||
extension instanceof FirstElementFlow
|
||||
or
|
||||
extension instanceof FabricExecuteExtension
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,5 @@ def command_execution_unsafe(request):
|
||||
def command_execution_safe(request):
|
||||
if request.method == 'POST':
|
||||
action = request.POST.get('action', '')
|
||||
#GOOD -- Use a whitelist
|
||||
#GOOD -- Use an allowlist
|
||||
subprocess.call(["application", COMMANDS[action]])
|
||||
|
||||
@@ -16,7 +16,7 @@ import python
|
||||
import Shadowing
|
||||
import semmle.python.types.Builtins
|
||||
|
||||
predicate white_list(string name) {
|
||||
predicate allow_list(string name) {
|
||||
/* These are rarely used and thus unlikely to be confusing */
|
||||
name = "iter" or
|
||||
name = "next" or
|
||||
@@ -51,7 +51,7 @@ predicate shadows(Name d, string name, Function scope, int line) {
|
||||
) and
|
||||
d.getScope() = scope and
|
||||
d.getLocation().getStartLine() = line and
|
||||
not white_list(name) and
|
||||
not allow_list(name) and
|
||||
not optimizing_parameter(d)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,27 @@ predicate mutates_globals(ModuleValue m) {
|
||||
exists(SubscriptNode sub | sub.getObject() = globals and sub.isStore())
|
||||
)
|
||||
or
|
||||
exists(Value enum_convert, ClassValue enum_class |
|
||||
// Enum (added in 3.4) has method `_convert_` that alters globals
|
||||
// This was called `_convert` until 3.8, but that name will be removed in 3.9
|
||||
exists(ClassValue enum_class |
|
||||
enum_class.getASuperType() = Value::named("enum.Enum") and
|
||||
enum_convert = enum_class.attr("_convert") and
|
||||
exists(CallNode call | call.getScope() = m.getScope() |
|
||||
enum_convert.getACall() = call or
|
||||
call.getFunction().pointsTo(enum_convert)
|
||||
(
|
||||
// In Python < 3.8, Enum._convert can be found with points-to
|
||||
exists(Value enum_convert |
|
||||
enum_convert = enum_class.attr("_convert") and
|
||||
exists(CallNode call | call.getScope() = m.getScope() |
|
||||
enum_convert.getACall() = call or
|
||||
call.getFunction().pointsTo(enum_convert)
|
||||
)
|
||||
)
|
||||
or
|
||||
// In Python 3.8, Enum._convert_ is implemented using a metaclass, and our points-to
|
||||
// analysis doesn't handle that well enough. So we need a special case for this
|
||||
not exists(Value enum_convert | enum_convert = enum_class.attr("_convert")) and
|
||||
exists(CallNode call | call.getScope() = m.getScope() |
|
||||
call.getFunction().(AttrNode).getObject(["_convert", "_convert_"]).pointsTo() =
|
||||
enum_class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @name Sanity check
|
||||
* @description General sanity check to be run on any and all code. Should never produce any results.
|
||||
* @id py/sanity-check
|
||||
* @name Consistency check
|
||||
* @description General consistency check to be run on any and all code. Should never produce any results.
|
||||
* @id py/consistency-check
|
||||
*/
|
||||
|
||||
import python
|
||||
@@ -24,7 +24,7 @@ predicate uniqueness_error(int number, string what, string problem) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate ast_sanity(string clsname, string problem, string what) {
|
||||
predicate ast_consistency(string clsname, string problem, string what) {
|
||||
exists(AstNode a | clsname = a.getAQlClass() |
|
||||
uniqueness_error(count(a.toString()), "toString", problem) and
|
||||
what = "at " + a.getLocation().toString()
|
||||
@@ -39,7 +39,7 @@ predicate ast_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate location_sanity(string clsname, string problem, string what) {
|
||||
predicate location_consistency(string clsname, string problem, string what) {
|
||||
exists(Location l | clsname = l.getAQlClass() |
|
||||
uniqueness_error(count(l.toString()), "toString", problem) and what = "at " + l.toString()
|
||||
or
|
||||
@@ -65,7 +65,7 @@ predicate location_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate cfg_sanity(string clsname, string problem, string what) {
|
||||
predicate cfg_consistency(string clsname, string problem, string what) {
|
||||
exists(ControlFlowNode f | clsname = f.getAQlClass() |
|
||||
uniqueness_error(count(f.getNode()), "getNode", problem) and
|
||||
what = "at " + f.getLocation().toString()
|
||||
@@ -80,7 +80,7 @@ predicate cfg_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate scope_sanity(string clsname, string problem, string what) {
|
||||
predicate scope_consistency(string clsname, string problem, string what) {
|
||||
exists(Scope s | clsname = s.getAQlClass() |
|
||||
uniqueness_error(count(s.getEntryNode()), "getEntryNode", problem) and
|
||||
what = "at " + s.getLocation().toString()
|
||||
@@ -125,7 +125,7 @@ private predicate introspected_builtin_object(Object o) {
|
||||
py_cobject_sources(o, 0)
|
||||
}
|
||||
|
||||
predicate builtin_object_sanity(string clsname, string problem, string what) {
|
||||
predicate builtin_object_consistency(string clsname, string problem, string what) {
|
||||
exists(Object o |
|
||||
clsname = o.getAQlClass() and
|
||||
what = best_description_builtin_object(o) and
|
||||
@@ -146,7 +146,7 @@ predicate builtin_object_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate source_object_sanity(string clsname, string problem, string what) {
|
||||
predicate source_object_consistency(string clsname, string problem, string what) {
|
||||
exists(Object o | clsname = o.getAQlClass() and not o.isBuiltin() |
|
||||
uniqueness_error(count(o.getOrigin()), "getOrigin", problem) and
|
||||
what = "at " + o.getOrigin().getLocation().toString()
|
||||
@@ -161,7 +161,7 @@ predicate source_object_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate ssa_sanity(string clsname, string problem, string what) {
|
||||
predicate ssa_consistency(string clsname, string problem, string what) {
|
||||
/* Zero or one definitions of each SSA variable */
|
||||
exists(SsaVariable var | clsname = var.getAQlClass() |
|
||||
uniqueness_error(strictcount(var.getDefinition()), "getDefinition", problem) and
|
||||
@@ -196,7 +196,7 @@ predicate ssa_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate function_object_sanity(string clsname, string problem, string what) {
|
||||
predicate function_object_consistency(string clsname, string problem, string what) {
|
||||
exists(FunctionObject func | clsname = func.getAQlClass() |
|
||||
what = func.getName() and
|
||||
(
|
||||
@@ -229,7 +229,7 @@ predicate intermediate_origins(ControlFlowNode use, ControlFlowNode inter, Objec
|
||||
)
|
||||
}
|
||||
|
||||
predicate points_to_sanity(string clsname, string problem, string what) {
|
||||
predicate points_to_consistency(string clsname, string problem, string what) {
|
||||
exists(Object obj |
|
||||
multiple_origins_per_object(obj) and
|
||||
clsname = obj.getAQlClass() and
|
||||
@@ -245,7 +245,7 @@ predicate points_to_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate jump_to_definition_sanity(string clsname, string problem, string what) {
|
||||
predicate jump_to_definition_consistency(string clsname, string problem, string what) {
|
||||
problem = "multiple (jump-to) definitions" and
|
||||
exists(Expr use |
|
||||
strictcount(getUniqueDefinition(use)) > 1 and
|
||||
@@ -254,7 +254,7 @@ predicate jump_to_definition_sanity(string clsname, string problem, string what)
|
||||
)
|
||||
}
|
||||
|
||||
predicate file_sanity(string clsname, string problem, string what) {
|
||||
predicate file_consistency(string clsname, string problem, string what) {
|
||||
exists(File file, Folder folder |
|
||||
clsname = file.getAQlClass() and
|
||||
problem = "has same name as a folder" and
|
||||
@@ -269,7 +269,7 @@ predicate file_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate class_value_sanity(string clsname, string problem, string what) {
|
||||
predicate class_value_consistency(string clsname, string problem, string what) {
|
||||
exists(ClassValue value, ClassValue sup, string attr |
|
||||
what = value.getName() and
|
||||
sup = value.getASuperType() and
|
||||
@@ -283,16 +283,16 @@ predicate class_value_sanity(string clsname, string problem, string what) {
|
||||
|
||||
from string clsname, string problem, string what
|
||||
where
|
||||
ast_sanity(clsname, problem, what) or
|
||||
location_sanity(clsname, problem, what) or
|
||||
scope_sanity(clsname, problem, what) or
|
||||
cfg_sanity(clsname, problem, what) or
|
||||
ssa_sanity(clsname, problem, what) or
|
||||
builtin_object_sanity(clsname, problem, what) or
|
||||
source_object_sanity(clsname, problem, what) or
|
||||
function_object_sanity(clsname, problem, what) or
|
||||
points_to_sanity(clsname, problem, what) or
|
||||
jump_to_definition_sanity(clsname, problem, what) or
|
||||
file_sanity(clsname, problem, what) or
|
||||
class_value_sanity(clsname, problem, what)
|
||||
ast_consistency(clsname, problem, what) or
|
||||
location_consistency(clsname, problem, what) or
|
||||
scope_consistency(clsname, problem, what) or
|
||||
cfg_consistency(clsname, problem, what) or
|
||||
ssa_consistency(clsname, problem, what) or
|
||||
builtin_object_consistency(clsname, problem, what) or
|
||||
source_object_consistency(clsname, problem, what) or
|
||||
function_object_consistency(clsname, problem, what) or
|
||||
points_to_consistency(clsname, problem, what) or
|
||||
jump_to_definition_consistency(clsname, problem, what) or
|
||||
file_consistency(clsname, problem, what) or
|
||||
class_value_consistency(clsname, problem, what)
|
||||
select clsname + " " + what + " has " + problem
|
||||
@@ -7,7 +7,7 @@ import DefinitionTracking
|
||||
|
||||
predicate want_to_have_definition(Expr e) {
|
||||
/* not builtin object like len, tuple, etc. */
|
||||
not exists(Object cobj | e.refersTo(cobj) and cobj.isC()) and
|
||||
not exists(Value builtin | e.pointsTo(builtin) and builtin.isBuiltin()) and
|
||||
(
|
||||
e instanceof Name and e.(Name).getCtx() instanceof Load
|
||||
or
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
- description: Security-and-quality queries for Python
|
||||
- qlpack: codeql-python
|
||||
- apply: security-and-quality-selectors.yml
|
||||
from: codeql-suite-helpers
|
||||
4
python/ql/src/codeql-suites/python-security-extended.qls
Normal file
4
python/ql/src/codeql-suites/python-security-extended.qls
Normal file
@@ -0,0 +1,4 @@
|
||||
- description: Security-extended queries for Python
|
||||
- qlpack: codeql-python
|
||||
- apply: security-extended-selectors.yml
|
||||
from: codeql-suite-helpers
|
||||
30
python/ql/src/experimental/CWE-643/xpath.qhelp
Normal file
30
python/ql/src/experimental/CWE-643/xpath.qhelp
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Using user-supplied information to construct an XPath query for XML data can
|
||||
result in an XPath injection flaw. By sending intentionally malformed information,
|
||||
an attacker can access data that he may not normally have access to.
|
||||
He/She may even be able to elevate his privileges on the web site if the XML data
|
||||
is being used for authentication (such as an XML based user file).
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>
|
||||
XPath injection can be prevented using parameterized XPath interface or escaping the user input to make it safe to include in a dynamically constructed query.
|
||||
If you are using quotes to terminate untrusted input in a dynamically constructed XPath query, then you need to escape that quote in the untrusted input to ensure the untrusted data can’t try to break out of that quoted context.
|
||||
</p>
|
||||
<p>
|
||||
Another better mitigation option is to use a precompiled XPath query. Precompiled XPath queries are already preset before the program executes, rather than created on the fly after the user’s input has been added to the string. This is a better route because you don’t have to worry about missing a character that should have been escaped.
|
||||
</p>
|
||||
</recommendation>
|
||||
<example>
|
||||
<p>In the example below, the xpath query is controlled by the user and hence leads to a vulnerability.</p>
|
||||
<sample src="xpathBad.py" />
|
||||
<p> This can be fixed by using a parameterized query as shown below.</p>
|
||||
<sample src="xpathGood.py" />
|
||||
</example>
|
||||
<references>
|
||||
<li>OWASP XPath injection : <a href="https://owasp.org/www-community/attacks/XPATH_Injection"></a>/>> </li>
|
||||
</references>
|
||||
</qhelp>
|
||||
35
python/ql/src/experimental/CWE-643/xpath.ql
Normal file
35
python/ql/src/experimental/CWE-643/xpath.ql
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @name XPath query built from user-controlled sources
|
||||
* @description Building a XPath query from user-controlled sources is vulnerable to insertion of
|
||||
* malicious Xpath code by the user.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
* @id py/xpath-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-643
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.security.Paths
|
||||
/* Sources */
|
||||
import semmle.python.web.HttpRequest
|
||||
/* Sinks */
|
||||
import experimental.semmle.python.security.injection.Xpath
|
||||
|
||||
class XpathInjectionConfiguration extends TaintTracking::Configuration {
|
||||
XpathInjectionConfiguration() { this = "Xpath injection configuration" }
|
||||
|
||||
override predicate isSource(TaintTracking::Source source) {
|
||||
source instanceof HttpRequestTaintSource
|
||||
}
|
||||
|
||||
override predicate isSink(TaintTracking::Sink sink) {
|
||||
sink instanceof XpathInjection::XpathInjectionSink
|
||||
}
|
||||
}
|
||||
|
||||
from XpathInjectionConfiguration config, TaintedPathSource src, TaintedPathSink sink
|
||||
where config.hasFlowPath(src, sink)
|
||||
select sink.getSink(), src, sink, "This Xpath query depends on $@.", src.getSource(),
|
||||
"a user-provided value"
|
||||
18
python/ql/src/experimental/CWE-643/xpathBad.py
Normal file
18
python/ql/src/experimental/CWE-643/xpathBad.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from lxml import etree
|
||||
from io import StringIO
|
||||
|
||||
from django.urls import path
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context, Engine, engines
|
||||
|
||||
|
||||
def a(request):
|
||||
value = request.GET['xpath']
|
||||
f = StringIO('<foo><bar></bar></foo>')
|
||||
tree = etree.parse(f)
|
||||
r = tree.xpath("/tag[@id='%s']" % value)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('a', a)
|
||||
]
|
||||
18
python/ql/src/experimental/CWE-643/xpathGood.py
Normal file
18
python/ql/src/experimental/CWE-643/xpathGood.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from lxml import etree
|
||||
from io import StringIO
|
||||
|
||||
from django.urls import path
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context, Engine, engines
|
||||
|
||||
|
||||
def a(request):
|
||||
value = request.GET['xpath']
|
||||
f = StringIO('<foo><bar></bar></foo>')
|
||||
tree = etree.parse(f)
|
||||
r = tree.xpath("/tag[@id=$tagid]", tagid=value)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('a', a)
|
||||
]
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Provides class and predicates to track external data that
|
||||
* may represent malicious xpath query objects.
|
||||
*
|
||||
* This module is intended to be imported into a taint-tracking query
|
||||
* to extend `TaintKind` and `TaintSink`.
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.TaintTracking
|
||||
import semmle.python.web.HttpRequest
|
||||
|
||||
/** Models Xpath Injection related classes and functions */
|
||||
module XpathInjection {
|
||||
/** Returns a class value which refers to `lxml.etree` */
|
||||
Value etree() { result = Value::named("lxml.etree") }
|
||||
|
||||
/** Returns a class value which refers to `lxml.etree` */
|
||||
Value libxml2parseFile() { result = Value::named("libxml2.parseFile") }
|
||||
|
||||
/** A generic taint sink that is vulnerable to Xpath injection. */
|
||||
abstract class XpathInjectionSink extends TaintSink { }
|
||||
|
||||
/**
|
||||
* A Sink representing an argument to the `etree.Xpath` call.
|
||||
*
|
||||
* from lxml import etree
|
||||
* root = etree.XML("<xmlContent>")
|
||||
* find_text = etree.XPath("`sink`")
|
||||
*/
|
||||
private class EtreeXpathArgument extends XpathInjectionSink {
|
||||
override string toString() { result = "lxml.etree.Xpath" }
|
||||
|
||||
EtreeXpathArgument() {
|
||||
exists(CallNode call | call.getFunction().(AttrNode).getObject("XPath").pointsTo(etree()) |
|
||||
call.getArg(0) = this
|
||||
)
|
||||
}
|
||||
|
||||
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Sink representing an argument to the `etree.EtXpath` call.
|
||||
*
|
||||
* from lxml import etree
|
||||
* root = etree.XML("<xmlContent>")
|
||||
* find_text = etree.EtXPath("`sink`")
|
||||
*/
|
||||
private class EtreeETXpathArgument extends XpathInjectionSink {
|
||||
override string toString() { result = "lxml.etree.ETXpath" }
|
||||
|
||||
EtreeETXpathArgument() {
|
||||
exists(CallNode call | call.getFunction().(AttrNode).getObject("ETXPath").pointsTo(etree()) |
|
||||
call.getArg(0) = this
|
||||
)
|
||||
}
|
||||
|
||||
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Sink representing an argument to the `xpath` call to a parsed xml document.
|
||||
*
|
||||
* from lxml import etree
|
||||
* from io import StringIO
|
||||
* f = StringIO('<foo><bar></bar></foo>')
|
||||
* tree = etree.parse(f)
|
||||
* r = tree.xpath('`sink`')
|
||||
*/
|
||||
private class ParseXpathArgument extends XpathInjectionSink {
|
||||
override string toString() { result = "lxml.etree.parse.xpath" }
|
||||
|
||||
ParseXpathArgument() {
|
||||
exists(
|
||||
CallNode parseCall, CallNode xpathCall, ControlFlowNode obj, Variable var, AssignStmt assign
|
||||
|
|
||||
parseCall.getFunction().(AttrNode).getObject("parse").pointsTo(etree()) and
|
||||
assign.getValue().(Call).getAFlowNode() = parseCall and
|
||||
xpathCall.getFunction().(AttrNode).getObject("xpath") = obj and
|
||||
var.getAUse() = obj and
|
||||
assign.getATarget() = var.getAStore() and
|
||||
xpathCall.getArg(0) = this
|
||||
)
|
||||
}
|
||||
|
||||
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Sink representing an argument to the `xpathEval` call to a parsed libxml2 document.
|
||||
*
|
||||
* import libxml2
|
||||
* tree = libxml2.parseFile("file.xml")
|
||||
* r = tree.xpathEval('`sink`')
|
||||
*/
|
||||
private class ParseFileXpathEvalArgument extends XpathInjectionSink {
|
||||
override string toString() { result = "libxml2.parseFile.xpathEval" }
|
||||
|
||||
ParseFileXpathEvalArgument() {
|
||||
exists(
|
||||
CallNode parseCall, CallNode xpathCall, ControlFlowNode obj, Variable var, AssignStmt assign
|
||||
|
|
||||
parseCall.getFunction().(AttrNode).pointsTo(libxml2parseFile()) and
|
||||
assign.getValue().(Call).getAFlowNode() = parseCall and
|
||||
xpathCall.getFunction().(AttrNode).getObject("xpathEval") = obj and
|
||||
var.getAUse() = obj and
|
||||
assign.getATarget() = var.getAStore() and
|
||||
xpathCall.getArg(0) = this
|
||||
)
|
||||
}
|
||||
|
||||
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
|
||||
}
|
||||
}
|
||||
2
python/ql/src/external/CodeDuplication.qll
vendored
2
python/ql/src/external/CodeDuplication.qll
vendored
@@ -268,6 +268,6 @@ predicate similarScopes(Scope s, Scope other, float percent, string message) {
|
||||
* Holds if the line is acceptable as a duplicate.
|
||||
* This is true for blocks of import statements.
|
||||
*/
|
||||
predicate whitelistedLineForDuplication(File f, int line) {
|
||||
predicate allowlistedLineForDuplication(File f, int line) {
|
||||
exists(ImportingStmt i | i.getLocation().getFile() = f and i.getLocation().getStartLine() = line)
|
||||
}
|
||||
|
||||
@@ -517,7 +517,14 @@ class ClassValue extends Value {
|
||||
/** Holds if this class is a container(). That is, does it have a __getitem__ method. */
|
||||
predicate isContainer() { exists(this.lookup("__getitem__")) }
|
||||
|
||||
/** Holds if this class is probably a sequence. */
|
||||
/**
|
||||
* Holds if this class is a sequence. Mutually exclusive with `isMapping()`.
|
||||
*
|
||||
* Following the definition from
|
||||
* https://docs.python.org/3/glossary.html#term-sequence.
|
||||
* We don't look at the keys accepted by `__getitem__, but default to treating a class
|
||||
* as a sequence (so might treat some mappings as sequences).
|
||||
*/
|
||||
predicate isSequence() {
|
||||
/*
|
||||
* To determine whether something is a sequence or a mapping is not entirely clear,
|
||||
@@ -538,16 +545,26 @@ class ClassValue extends Value {
|
||||
or
|
||||
major_version() = 3 and this.getASuperType() = Value::named("collections.abc.Sequence")
|
||||
or
|
||||
/* Does it have an index or __reversed__ method? */
|
||||
this.isContainer() and
|
||||
(
|
||||
this.hasAttribute("index") or
|
||||
this.hasAttribute("__reversed__")
|
||||
)
|
||||
this.hasAttribute("__getitem__") and
|
||||
this.hasAttribute("__len__") and
|
||||
not this.getASuperType() = ClassValue::dict() and
|
||||
not this.getASuperType() = Value::named("collections.Mapping") and
|
||||
not this.getASuperType() = Value::named("collections.abc.Mapping")
|
||||
}
|
||||
|
||||
/** Holds if this class is a mapping. */
|
||||
/**
|
||||
* Holds if this class is a mapping. Mutually exclusive with `isSequence()`.
|
||||
*
|
||||
* Although a class will satisfy the requirement by the definition in
|
||||
* https://docs.python.org/3.8/glossary.html#term-mapping, we don't look at the keys
|
||||
* accepted by `__getitem__, but default to treating a class as a sequence (so might
|
||||
* treat some mappings as sequences).
|
||||
*/
|
||||
predicate isMapping() {
|
||||
major_version() = 2 and this.getASuperType() = Value::named("collections.Mapping")
|
||||
or
|
||||
major_version() = 3 and this.getASuperType() = Value::named("collections.abc.Mapping")
|
||||
or
|
||||
this.hasAttribute("__getitem__") and
|
||||
not this.isSequence()
|
||||
}
|
||||
@@ -632,6 +649,10 @@ class ClassValue extends Value {
|
||||
* Note that this does not include other callables such as bound-methods.
|
||||
*/
|
||||
abstract class FunctionValue extends CallableValue {
|
||||
/**
|
||||
* Gets the qualified name for this function.
|
||||
* Should return the same name as the `__qualname__` attribute on functions in Python 3.
|
||||
*/
|
||||
abstract string getQualifiedName();
|
||||
|
||||
/** Gets a longer, more descriptive version of toString() */
|
||||
|
||||
@@ -28,7 +28,8 @@ predicate used_as_regex(Expr s, string mode) {
|
||||
/* Call to re.xxx(regex, ... [mode]) */
|
||||
exists(CallNode call, string name |
|
||||
call.getArg(0).refersTo(_, _, s.getAFlowNode()) and
|
||||
call.getFunction().pointsTo(Module::named("re").attr(name))
|
||||
call.getFunction().pointsTo(Module::named("re").attr(name)) and
|
||||
not name = "escape"
|
||||
|
|
||||
mode = "None"
|
||||
or
|
||||
@@ -124,16 +125,40 @@ abstract class RegexString extends Expr {
|
||||
)
|
||||
}
|
||||
|
||||
/** Named unicode characters, eg \N{degree sign} */
|
||||
private predicate escapedName(int start, int end) {
|
||||
this.escapingChar(start) and
|
||||
this.getChar(start + 1) = "N" and
|
||||
this.getChar(start + 2) = "{" and
|
||||
this.getChar(end - 1) = "}" and
|
||||
end > start and
|
||||
not exists(int i | start + 2 < i and i < end - 1 |
|
||||
this.getChar(i) = "}"
|
||||
)
|
||||
}
|
||||
|
||||
private predicate escapedCharacter(int start, int end) {
|
||||
this.escapingChar(start) and
|
||||
not exists(this.getText().substring(start + 1, end + 1).toInt()) and
|
||||
(
|
||||
// hex value \xhh
|
||||
this.getChar(start + 1) = "x" and end = start + 4
|
||||
or
|
||||
// octal value \ooo
|
||||
end in [start + 2 .. start + 4] and
|
||||
exists(this.getText().substring(start + 1, end).toInt())
|
||||
or
|
||||
this.getChar(start + 1) != "x" and end = start + 2
|
||||
// 16-bit hex value \uhhhh
|
||||
this.getChar(start + 1) = "u" and end = start + 6
|
||||
or
|
||||
// 32-bit hex value \Uhhhhhhhh
|
||||
this.getChar(start + 1) = "U" and end = start + 10
|
||||
or
|
||||
escapedName(start, end)
|
||||
or
|
||||
// escape not handled above, update when adding a new case
|
||||
not this.getChar(start + 1) in ["x", "u", "U", "N"] and
|
||||
end = start + 2
|
||||
)
|
||||
}
|
||||
|
||||
@@ -472,8 +497,12 @@ abstract class RegexString extends Expr {
|
||||
this.getChar(endin) = "}" and
|
||||
end > start and
|
||||
exists(string multiples | multiples = this.getText().substring(start + 1, endin) |
|
||||
multiples.regexpMatch("0+") and maybe_empty = true
|
||||
or
|
||||
multiples.regexpMatch("0*,[0-9]*") and maybe_empty = true
|
||||
or
|
||||
multiples.regexpMatch("0*[1-9][0-9]*") and maybe_empty = false
|
||||
or
|
||||
multiples.regexpMatch("0*[1-9][0-9]*,[0-9]*") and maybe_empty = false
|
||||
) and
|
||||
not exists(int mid |
|
||||
@@ -618,9 +647,13 @@ abstract class RegexString extends Expr {
|
||||
start = 0 and end = this.getText().length()
|
||||
or
|
||||
exists(int y | this.lastPart(start, y) |
|
||||
this.emptyMatchAtEndGroup(end, y) or
|
||||
this.qualifiedItem(end, y, true) or
|
||||
this.emptyMatchAtEndGroup(end, y)
|
||||
or
|
||||
this.qualifiedItem(end, y, true)
|
||||
or
|
||||
this.specialCharacter(end, y, "$")
|
||||
or
|
||||
y = end + 2 and this.escapingChar(end) and this.getChar(end + 1) = "Z"
|
||||
)
|
||||
or
|
||||
exists(int x |
|
||||
|
||||
@@ -231,3 +231,41 @@ class FabricV1Commands extends CommandSink {
|
||||
|
||||
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* An extension that propagates taint from the arguments of `fabric.api.execute(func, arg0, arg1, ...)`
|
||||
* to the parameters of `func`, since this will call `func(arg0, arg1, ...)`.
|
||||
*/
|
||||
class FabricExecuteExtension extends DataFlowExtension::DataFlowNode {
|
||||
CallNode call;
|
||||
|
||||
FabricExecuteExtension() {
|
||||
call = Value::named("fabric.api.execute").getACall() and
|
||||
(
|
||||
this = call.getArg(any(int i | i > 0))
|
||||
or
|
||||
this = call.getArgByName(any(string s | not s = "task"))
|
||||
)
|
||||
}
|
||||
|
||||
override ControlFlowNode getASuccessorNode(TaintKind fromkind, TaintKind tokind) {
|
||||
tokind = fromkind and
|
||||
exists(CallableValue func |
|
||||
(
|
||||
call.getArg(0).pointsTo(func)
|
||||
or
|
||||
call.getArgByName("task").pointsTo(func)
|
||||
) and
|
||||
exists(int i |
|
||||
// execute(func, arg0, arg1) => func(arg0, arg1)
|
||||
this = call.getArg(i) and
|
||||
result = func.getParameter(i - 1)
|
||||
)
|
||||
or
|
||||
exists(string name |
|
||||
this = call.getArgByName(name) and
|
||||
result = func.getParameterByName(name)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
| mapping | builtin-class collections.defaultdict |
|
||||
| mapping | builtin-class dict |
|
||||
| mapping | class MyDictSubclass |
|
||||
| mapping | class MyMappingABC |
|
||||
| mapping | class OrderedDict |
|
||||
| neither sequence nor mapping | builtin-class set |
|
||||
| sequence | builtin-class list |
|
||||
| sequence | builtin-class str |
|
||||
| sequence | builtin-class tuple |
|
||||
| sequence | builtin-class unicode |
|
||||
| sequence | class MySequenceABC |
|
||||
| sequence | class MySequenceImpl |
|
||||
@@ -0,0 +1,20 @@
|
||||
import python
|
||||
|
||||
from ClassValue cls, string res
|
||||
where
|
||||
exists(CallNode call |
|
||||
call.getFunction().(NameNode).getId() = "test" and
|
||||
call.getAnArg().pointsTo(cls)
|
||||
) and
|
||||
(
|
||||
cls.isSequence() and
|
||||
cls.isMapping() and
|
||||
res = "IS BOTH. SHOULD NOT HAPPEN. THEY ARE MUTUALLY EXCLUSIVE."
|
||||
or
|
||||
cls.isSequence() and not cls.isMapping() and res = "sequence"
|
||||
or
|
||||
not cls.isSequence() and cls.isMapping() and res = "mapping"
|
||||
or
|
||||
not cls.isSequence() and not cls.isMapping() and res = "neither sequence nor mapping"
|
||||
)
|
||||
select res, cls.toString()
|
||||
@@ -0,0 +1 @@
|
||||
semmle-extractor-options: --lang=2 --max-import-depth=2
|
||||
@@ -0,0 +1,50 @@
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
# Python 2 specific
|
||||
from collections import Sequence, Mapping
|
||||
|
||||
def test(*args):
|
||||
pass
|
||||
|
||||
class MySequenceABC(Sequence):
|
||||
pass
|
||||
|
||||
class MyMappingABC(Mapping):
|
||||
pass
|
||||
|
||||
class MySequenceImpl(object):
|
||||
def __getitem__(self, key):
|
||||
pass
|
||||
|
||||
def __len__(self):
|
||||
pass
|
||||
|
||||
class MyDictSubclass(dict):
|
||||
pass
|
||||
|
||||
test(
|
||||
list,
|
||||
tuple,
|
||||
str,
|
||||
unicode,
|
||||
bytes,
|
||||
MySequenceABC,
|
||||
MySequenceImpl,
|
||||
set,
|
||||
dict,
|
||||
OrderedDict,
|
||||
defaultdict,
|
||||
MyMappingABC,
|
||||
MyDictSubclass,
|
||||
)
|
||||
|
||||
for seq_cls in (list, tuple, str, bytes):
|
||||
assert issubclass(seq_cls, collections.abc.Sequence)
|
||||
assert not issubclass(seq_cls, collections.abc.Mapping)
|
||||
|
||||
for map_cls in (dict, OrderedDict, defaultdict):
|
||||
assert not issubclass(map_cls, collections.abc.Sequence)
|
||||
assert issubclass(map_cls, collections.abc.Mapping)
|
||||
|
||||
assert not issubclass(set, collections.abc.Sequence)
|
||||
assert not issubclass(set, collections.abc.Mapping)
|
||||
@@ -0,0 +1,13 @@
|
||||
| mapping | builtin-class collections.OrderedDict |
|
||||
| mapping | builtin-class collections.defaultdict |
|
||||
| mapping | builtin-class dict |
|
||||
| mapping | class MyDictSubclass |
|
||||
| mapping | class MyMappingABC |
|
||||
| mapping | class OrderedDict |
|
||||
| neither sequence nor mapping | builtin-class set |
|
||||
| sequence | builtin-class bytes |
|
||||
| sequence | builtin-class list |
|
||||
| sequence | builtin-class str |
|
||||
| sequence | builtin-class tuple |
|
||||
| sequence | class MySequenceABC |
|
||||
| sequence | class MySequenceImpl |
|
||||
@@ -0,0 +1,20 @@
|
||||
import python
|
||||
|
||||
from ClassValue cls, string res
|
||||
where
|
||||
exists(CallNode call |
|
||||
call.getFunction().(NameNode).getId() = "test" and
|
||||
call.getAnArg().pointsTo(cls)
|
||||
) and
|
||||
(
|
||||
cls.isSequence() and
|
||||
cls.isMapping() and
|
||||
res = "IS BOTH. SHOULD NOT HAPPEN. THEY ARE MUTUALLY EXCLUSIVE."
|
||||
or
|
||||
cls.isSequence() and not cls.isMapping() and res = "sequence"
|
||||
or
|
||||
not cls.isSequence() and cls.isMapping() and res = "mapping"
|
||||
or
|
||||
not cls.isSequence() and not cls.isMapping() and res = "neither sequence nor mapping"
|
||||
)
|
||||
select res, cls.toString()
|
||||
@@ -0,0 +1 @@
|
||||
semmle-extractor-options: --max-import-depth=2
|
||||
@@ -0,0 +1,50 @@
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
# Python 3 specific
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
def test(*args):
|
||||
pass
|
||||
|
||||
class MySequenceABC(Sequence):
|
||||
pass
|
||||
|
||||
class MyMappingABC(Mapping):
|
||||
pass
|
||||
|
||||
class MySequenceImpl(object):
|
||||
def __getitem__(self, key):
|
||||
pass
|
||||
|
||||
def __len__(self):
|
||||
pass
|
||||
|
||||
class MyDictSubclass(dict):
|
||||
pass
|
||||
|
||||
test(
|
||||
list,
|
||||
tuple,
|
||||
str,
|
||||
unicode,
|
||||
bytes,
|
||||
MySequenceABC,
|
||||
MySequenceImpl,
|
||||
set,
|
||||
dict,
|
||||
OrderedDict,
|
||||
defaultdict,
|
||||
MyMappingABC,
|
||||
MyDictSubclass,
|
||||
)
|
||||
|
||||
for seq_cls in (list, tuple, str, bytes):
|
||||
assert issubclass(seq_cls, collections.abc.Sequence)
|
||||
assert not issubclass(seq_cls, collections.abc.Mapping)
|
||||
|
||||
for map_cls in (dict, OrderedDict, defaultdict):
|
||||
assert not issubclass(map_cls, collections.abc.Sequence)
|
||||
assert issubclass(map_cls, collections.abc.Mapping)
|
||||
|
||||
assert not issubclass(set, collections.abc.Sequence)
|
||||
assert not issubclass(set, collections.abc.Mapping)
|
||||
1
python/ql/test/experimental/CWE-643/options
Normal file
1
python/ql/test/experimental/CWE-643/options
Normal file
@@ -0,0 +1 @@
|
||||
semmle-extractor-options: --max-import-depth=3 -p ../../query-tests/Security/lib/
|
||||
38
python/ql/test/experimental/CWE-643/xpath.expected
Normal file
38
python/ql/test/experimental/CWE-643/xpath.expected
Normal file
@@ -0,0 +1,38 @@
|
||||
edges
|
||||
| xpathBad.py:9:7:9:13 | django.request.HttpRequest | xpathBad.py:10:13:10:19 | django.request.HttpRequest |
|
||||
| xpathBad.py:9:7:9:13 | django.request.HttpRequest | xpathBad.py:10:13:10:19 | django.request.HttpRequest |
|
||||
| xpathBad.py:10:13:10:19 | django.request.HttpRequest | xpathBad.py:10:13:10:23 | django.http.request.QueryDict |
|
||||
| xpathBad.py:10:13:10:19 | django.request.HttpRequest | xpathBad.py:10:13:10:23 | django.http.request.QueryDict |
|
||||
| xpathBad.py:10:13:10:23 | django.http.request.QueryDict | xpathBad.py:10:13:10:32 | externally controlled string |
|
||||
| xpathBad.py:10:13:10:23 | django.http.request.QueryDict | xpathBad.py:10:13:10:32 | externally controlled string |
|
||||
| xpathBad.py:10:13:10:32 | externally controlled string | xpathBad.py:13:39:13:43 | externally controlled string |
|
||||
| xpathBad.py:10:13:10:32 | externally controlled string | xpathBad.py:13:39:13:43 | externally controlled string |
|
||||
| xpathBad.py:13:39:13:43 | externally controlled string | xpathBad.py:13:20:13:43 | externally controlled string |
|
||||
| xpathBad.py:13:39:13:43 | externally controlled string | xpathBad.py:13:20:13:43 | externally controlled string |
|
||||
| xpathFlow.py:11:18:11:29 | dict of externally controlled string | xpathFlow.py:11:18:11:44 | externally controlled string |
|
||||
| xpathFlow.py:11:18:11:29 | dict of externally controlled string | xpathFlow.py:11:18:11:44 | externally controlled string |
|
||||
| xpathFlow.py:11:18:11:44 | externally controlled string | xpathFlow.py:14:20:14:29 | externally controlled string |
|
||||
| xpathFlow.py:11:18:11:44 | externally controlled string | xpathFlow.py:14:20:14:29 | externally controlled string |
|
||||
| xpathFlow.py:20:18:20:29 | dict of externally controlled string | xpathFlow.py:20:18:20:44 | externally controlled string |
|
||||
| xpathFlow.py:20:18:20:29 | dict of externally controlled string | xpathFlow.py:20:18:20:44 | externally controlled string |
|
||||
| xpathFlow.py:20:18:20:44 | externally controlled string | xpathFlow.py:23:29:23:38 | externally controlled string |
|
||||
| xpathFlow.py:20:18:20:44 | externally controlled string | xpathFlow.py:23:29:23:38 | externally controlled string |
|
||||
| xpathFlow.py:30:18:30:29 | dict of externally controlled string | xpathFlow.py:30:18:30:44 | externally controlled string |
|
||||
| xpathFlow.py:30:18:30:29 | dict of externally controlled string | xpathFlow.py:30:18:30:44 | externally controlled string |
|
||||
| xpathFlow.py:30:18:30:44 | externally controlled string | xpathFlow.py:32:29:32:38 | externally controlled string |
|
||||
| xpathFlow.py:30:18:30:44 | externally controlled string | xpathFlow.py:32:29:32:38 | externally controlled string |
|
||||
| xpathFlow.py:39:18:39:29 | dict of externally controlled string | xpathFlow.py:39:18:39:44 | externally controlled string |
|
||||
| xpathFlow.py:39:18:39:29 | dict of externally controlled string | xpathFlow.py:39:18:39:44 | externally controlled string |
|
||||
| xpathFlow.py:39:18:39:44 | externally controlled string | xpathFlow.py:41:31:41:40 | externally controlled string |
|
||||
| xpathFlow.py:39:18:39:44 | externally controlled string | xpathFlow.py:41:31:41:40 | externally controlled string |
|
||||
| xpathFlow.py:47:18:47:29 | dict of externally controlled string | xpathFlow.py:47:18:47:44 | externally controlled string |
|
||||
| xpathFlow.py:47:18:47:29 | dict of externally controlled string | xpathFlow.py:47:18:47:44 | externally controlled string |
|
||||
| xpathFlow.py:47:18:47:44 | externally controlled string | xpathFlow.py:49:29:49:38 | externally controlled string |
|
||||
| xpathFlow.py:47:18:47:44 | externally controlled string | xpathFlow.py:49:29:49:38 | externally controlled string |
|
||||
#select
|
||||
| xpathBad.py:13:20:13:43 | BinaryExpr | xpathBad.py:9:7:9:13 | django.request.HttpRequest | xpathBad.py:13:20:13:43 | externally controlled string | This Xpath query depends on $@. | xpathBad.py:9:7:9:13 | request | a user-provided value |
|
||||
| xpathFlow.py:14:20:14:29 | xpathQuery | xpathFlow.py:11:18:11:29 | dict of externally controlled string | xpathFlow.py:14:20:14:29 | externally controlled string | This Xpath query depends on $@. | xpathFlow.py:11:18:11:29 | Attribute | a user-provided value |
|
||||
| xpathFlow.py:23:29:23:38 | xpathQuery | xpathFlow.py:20:18:20:29 | dict of externally controlled string | xpathFlow.py:23:29:23:38 | externally controlled string | This Xpath query depends on $@. | xpathFlow.py:20:18:20:29 | Attribute | a user-provided value |
|
||||
| xpathFlow.py:32:29:32:38 | xpathQuery | xpathFlow.py:30:18:30:29 | dict of externally controlled string | xpathFlow.py:32:29:32:38 | externally controlled string | This Xpath query depends on $@. | xpathFlow.py:30:18:30:29 | Attribute | a user-provided value |
|
||||
| xpathFlow.py:41:31:41:40 | xpathQuery | xpathFlow.py:39:18:39:29 | dict of externally controlled string | xpathFlow.py:41:31:41:40 | externally controlled string | This Xpath query depends on $@. | xpathFlow.py:39:18:39:29 | Attribute | a user-provided value |
|
||||
| xpathFlow.py:49:29:49:38 | xpathQuery | xpathFlow.py:47:18:47:29 | dict of externally controlled string | xpathFlow.py:49:29:49:38 | externally controlled string | This Xpath query depends on $@. | xpathFlow.py:47:18:47:29 | Attribute | a user-provided value |
|
||||
40
python/ql/test/experimental/CWE-643/xpath.py
Normal file
40
python/ql/test/experimental/CWE-643/xpath.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from lxml import etree
|
||||
from io import StringIO
|
||||
|
||||
|
||||
def a():
|
||||
f = StringIO('<foo><bar></bar></foo>')
|
||||
tree = etree.parse(f)
|
||||
r = tree.xpath('/foo/bar')
|
||||
|
||||
|
||||
def b():
|
||||
root = etree.XML("<root><a>TEXT</a></root>")
|
||||
find_text = etree.XPath("//text()")
|
||||
text = find_text(root)[0]
|
||||
|
||||
|
||||
def c():
|
||||
root = etree.XML("<root><a>TEXT</a></root>")
|
||||
find_text = etree.XPath("//text()", smart_strings=False)
|
||||
text = find_text(root)[0]
|
||||
|
||||
|
||||
def d():
|
||||
root = etree.XML("<root><a>TEXT</a></root>")
|
||||
find_text = find = etree.ETXPath("//{ns}b")
|
||||
text = find_text(root)[0]
|
||||
|
||||
|
||||
def e():
|
||||
import libxml2
|
||||
doc = libxml2.parseFile('xpath_injection/credential.xml')
|
||||
results = doc.xpathEval('sink')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a()
|
||||
b()
|
||||
c()
|
||||
d()
|
||||
e()
|
||||
1
python/ql/test/experimental/CWE-643/xpath.qlref
Normal file
1
python/ql/test/experimental/CWE-643/xpath.qlref
Normal file
@@ -0,0 +1 @@
|
||||
experimental/CWE-643/xpath.ql
|
||||
18
python/ql/test/experimental/CWE-643/xpathBad.py
Normal file
18
python/ql/test/experimental/CWE-643/xpathBad.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from lxml import etree
|
||||
from io import StringIO
|
||||
|
||||
from django.urls import path
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context, Engine, engines
|
||||
|
||||
|
||||
def a(request):
|
||||
value = request.GET['xpath']
|
||||
f = StringIO('<foo><bar></bar></foo>')
|
||||
tree = etree.parse(f)
|
||||
r = tree.xpath("/tag[@id='%s']" % value)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('a', a)
|
||||
]
|
||||
49
python/ql/test/experimental/CWE-643/xpathFlow.py
Normal file
49
python/ql/test/experimental/CWE-643/xpathFlow.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from io import StringIO
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/xpath1")
|
||||
def xpath1():
|
||||
from lxml import etree
|
||||
|
||||
xpathQuery = request.args.get('xml', '')
|
||||
f = StringIO('<foo><bar></bar></foo>')
|
||||
tree = etree.parse(f)
|
||||
r = tree.xpath(xpathQuery)
|
||||
|
||||
|
||||
@app.route("/xpath2")
|
||||
def xpath2():
|
||||
from lxml import etree
|
||||
xpathQuery = request.args.get('xml', '')
|
||||
|
||||
root = etree.XML("<root><a>TEXT</a></root>")
|
||||
find_text = etree.XPath(xpathQuery)
|
||||
text = find_text(root)[0]
|
||||
|
||||
|
||||
@app.route("/xpath3")
|
||||
def xpath3():
|
||||
from lxml import etree
|
||||
xpathQuery = request.args.get('xml', '')
|
||||
root = etree.XML("<root><a>TEXT</a></root>")
|
||||
find_text = etree.XPath(xpathQuery, smart_strings=False)
|
||||
text = find_text(root)[0]
|
||||
|
||||
|
||||
@app.route("/xpath4")
|
||||
def xpath4():
|
||||
from lxml import etree
|
||||
xpathQuery = request.args.get('xml', '')
|
||||
root = etree.XML("<root><a>TEXT</a></root>")
|
||||
find_text = etree.ETXPath(xpathQuery)
|
||||
text = find_text(root)[0]
|
||||
|
||||
@app.route("/xpath5")
|
||||
def xpath5():
|
||||
import libxml2
|
||||
xpathQuery = request.args.get('xml', '')
|
||||
doc = libxml2.parseFile('xpath_injection/credential.xml')
|
||||
results = doc.xpathEval(xpathQuery)
|
||||
18
python/ql/test/experimental/CWE-643/xpathGood.py
Normal file
18
python/ql/test/experimental/CWE-643/xpathGood.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from lxml import etree
|
||||
from io import StringIO
|
||||
|
||||
from django.urls import path
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context, Engine, engines
|
||||
|
||||
|
||||
def a(request):
|
||||
value = request.GET['xpath']
|
||||
f = StringIO('<foo><bar></bar></foo>')
|
||||
tree = etree.parse(f)
|
||||
r = tree.xpath("/tag[@id=$tagid]", tagid=value)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('a', a)
|
||||
]
|
||||
12
python/ql/test/experimental/CWE-643/xpathSinks.expected
Normal file
12
python/ql/test/experimental/CWE-643/xpathSinks.expected
Normal file
@@ -0,0 +1,12 @@
|
||||
| xpath.py:8:20:8:29 | lxml.etree.parse.xpath | externally controlled string |
|
||||
| xpath.py:13:29:13:38 | lxml.etree.Xpath | externally controlled string |
|
||||
| xpath.py:19:29:19:38 | lxml.etree.Xpath | externally controlled string |
|
||||
| xpath.py:25:38:25:46 | lxml.etree.ETXpath | externally controlled string |
|
||||
| xpath.py:32:29:32:34 | libxml2.parseFile.xpathEval | externally controlled string |
|
||||
| xpathBad.py:13:20:13:43 | lxml.etree.parse.xpath | externally controlled string |
|
||||
| xpathFlow.py:14:20:14:29 | lxml.etree.parse.xpath | externally controlled string |
|
||||
| xpathFlow.py:23:29:23:38 | lxml.etree.Xpath | externally controlled string |
|
||||
| xpathFlow.py:32:29:32:38 | lxml.etree.Xpath | externally controlled string |
|
||||
| xpathFlow.py:41:31:41:40 | lxml.etree.ETXpath | externally controlled string |
|
||||
| xpathFlow.py:49:29:49:38 | libxml2.parseFile.xpathEval | externally controlled string |
|
||||
| xpathGood.py:13:20:13:37 | lxml.etree.parse.xpath | externally controlled string |
|
||||
6
python/ql/test/experimental/CWE-643/xpathSinks.ql
Normal file
6
python/ql/test/experimental/CWE-643/xpathSinks.ql
Normal file
@@ -0,0 +1,6 @@
|
||||
import python
|
||||
import experimental.semmle.python.security.injection.Xpath
|
||||
|
||||
from XpathInjection::XpathInjectionSink sink, TaintKind kind
|
||||
where sink.sinks(kind)
|
||||
select sink, kind
|
||||
@@ -2,7 +2,7 @@ import python
|
||||
import semmle.python.pointsto.PointsTo
|
||||
import semmle.python.objects.ObjectInternal
|
||||
|
||||
predicate ssa_sanity(string clsname, string problem, string what) {
|
||||
predicate ssa_consistency(string clsname, string problem, string what) {
|
||||
/* Exactly one definition of each SSA variable */
|
||||
exists(EssaVariable var | clsname = var.getAQlClass() |
|
||||
/* Exactly one definition of each SSA variable */
|
||||
@@ -130,7 +130,7 @@ predicate ssa_sanity(string clsname, string problem, string what) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate undefined_sanity(string clsname, string problem, string what) {
|
||||
predicate undefined_consistency(string clsname, string problem, string what) {
|
||||
/* Variables may be undefined, but values cannot be */
|
||||
exists(ControlFlowNode f |
|
||||
PointsToInternal::pointsTo(f, _, ObjectInternal::undefined(), _) and
|
||||
@@ -142,5 +142,5 @@ predicate undefined_sanity(string clsname, string problem, string what) {
|
||||
}
|
||||
|
||||
from string clsname, string problem, string what
|
||||
where ssa_sanity(clsname, problem, what) or undefined_sanity(clsname, problem, what)
|
||||
where ssa_consistency(clsname, problem, what) or undefined_consistency(clsname, problem, what)
|
||||
select clsname, what, problem
|
||||
@@ -57,7 +57,7 @@ def loop(seq):
|
||||
if v:
|
||||
use(v)
|
||||
|
||||
#This was causing the sanity check to fail,
|
||||
#This was causing the consistency check to fail,
|
||||
def double_attr_check(x, y):
|
||||
if x.b == 3:
|
||||
return
|
||||
|
||||
@@ -95,7 +95,7 @@ def h():
|
||||
if not x:
|
||||
pass
|
||||
|
||||
def complex_test(x): # Was failing sanity check.
|
||||
def complex_test(x): # Was failing consistency check.
|
||||
if not (foo(x) and bar(x)):
|
||||
use(x)
|
||||
pass
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
| test.py:6:5:6:22 | Function Foo.foo | test.py:9:1:9:11 | ControlFlowNode for Attribute() |
|
||||
@@ -0,0 +1,4 @@
|
||||
import python
|
||||
|
||||
from PythonFunctionValue func
|
||||
select func, func.getACall()
|
||||
@@ -0,0 +1,25 @@
|
||||
# Simple classmethod
|
||||
|
||||
class Foo(object):
|
||||
|
||||
@classmethod
|
||||
def foo(cls, arg):
|
||||
print(cls, arg)
|
||||
|
||||
Foo.foo(42)
|
||||
|
||||
|
||||
# classmethod defined by metaclass
|
||||
|
||||
class BarMeta(type):
|
||||
|
||||
def bar(cls, arg):
|
||||
print(cls, arg)
|
||||
|
||||
class Bar(metaclass=BarMeta):
|
||||
pass
|
||||
|
||||
Bar.bar(42) # TODO: No points-to
|
||||
|
||||
# If this is solved, please update python/ql/src/Variables/UndefinedExport.ql which has a
|
||||
# work-around for this behavior
|
||||
@@ -110,7 +110,6 @@
|
||||
| ax{3,} | 5 | 6 |
|
||||
| ax{3} | 0 | 1 |
|
||||
| ax{3} | 1 | 2 |
|
||||
| ax{3} | 2 | 3 |
|
||||
| ax{3} | 3 | 4 |
|
||||
| ax{3} | 4 | 5 |
|
||||
| ax{,3} | 0 | 1 |
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
| ax{3,} | last | 1 | 6 |
|
||||
| ax{3,} | last | 5 | 6 |
|
||||
| ax{3} | first | 0 | 1 |
|
||||
| ax{3} | last | 1 | 2 |
|
||||
| ax{3} | last | 1 | 5 |
|
||||
| ax{3} | last | 4 | 5 |
|
||||
| ax{,3} | first | 0 | 1 |
|
||||
| ax{,3} | last | 0 | 1 |
|
||||
|
||||
@@ -11,4 +11,5 @@
|
||||
| ^[A-Z_]+$(?<!not-this) | 1 | 8 | false |
|
||||
| ax{01,3} | 1 | 8 | false |
|
||||
| ax{3,} | 1 | 6 | false |
|
||||
| ax{3} | 1 | 5 | false |
|
||||
| ax{,3} | 1 | 6 | true |
|
||||
|
||||
@@ -207,9 +207,9 @@
|
||||
| ax{3,} | sequence | 0 | 6 |
|
||||
| ax{3} | char | 0 | 1 |
|
||||
| ax{3} | char | 1 | 2 |
|
||||
| ax{3} | char | 2 | 3 |
|
||||
| ax{3} | char | 3 | 4 |
|
||||
| ax{3} | char | 4 | 5 |
|
||||
| ax{3} | qualified | 1 | 5 |
|
||||
| ax{3} | sequence | 0 | 5 |
|
||||
| ax{,3} | char | 0 | 1 |
|
||||
| ax{,3} | char | 1 | 2 |
|
||||
|
||||
@@ -62,3 +62,7 @@ re.compile(r'(?:(?P<n1>^(?:|x)))')
|
||||
re.compile(r"\[(?P<txt>[^[]*)\]\((?P<uri>[^)]*)")
|
||||
|
||||
re.compile("", re.M) # ODASA-8056
|
||||
|
||||
# FP reported in https://github.com/github/codeql/issues/3712
|
||||
# This does not define a regex (but could be used by other code to do so)
|
||||
escaped = re.escape("https://www.humblebundle.com/home/library")
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import python
|
||||
import semmle.python.dataflow.TaintTracking
|
||||
import semmle.python.security.strings.Untrusted
|
||||
import semmle.python.security.injection.Command
|
||||
|
||||
class SimpleSource extends TaintSource {
|
||||
SimpleSource() { this.(NameNode).getId() = "TAINTED_STRING" }
|
||||
|
||||
override predicate isSourceOf(TaintKind kind) { kind instanceof ExternalStringKind }
|
||||
|
||||
override string toString() { result = "taint source" }
|
||||
}
|
||||
|
||||
class FabricExecuteTestConfiguration extends TaintTracking::Configuration {
|
||||
FabricExecuteTestConfiguration() { this = "FabricExecuteTestConfiguration" }
|
||||
|
||||
override predicate isSource(TaintTracking::Source source) { source instanceof SimpleSource }
|
||||
|
||||
override predicate isSink(TaintTracking::Sink sink) { sink instanceof CommandSink }
|
||||
|
||||
override predicate isExtension(TaintTracking::Extension extension) {
|
||||
extension instanceof FabricExecuteExtension
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
| test.py:8 | ok | unsafe | cmd | externally controlled string |
|
||||
| test.py:8 | ok | unsafe | cmd2 | externally controlled string |
|
||||
| test.py:9 | ok | unsafe | safe_arg | <NO TAINT> |
|
||||
| test.py:9 | ok | unsafe | safe_optional | <NO TAINT> |
|
||||
| test.py:16 | ok | unsafe | cmd | externally controlled string |
|
||||
| test.py:16 | ok | unsafe | cmd2 | externally controlled string |
|
||||
| test.py:17 | ok | unsafe | safe_arg | <NO TAINT> |
|
||||
| test.py:17 | ok | unsafe | safe_optional | <NO TAINT> |
|
||||
| test.py:23 | ok | some_http_handler | cmd | externally controlled string |
|
||||
| test.py:23 | ok | some_http_handler | cmd2 | externally controlled string |
|
||||
@@ -0,0 +1,34 @@
|
||||
import python
|
||||
import semmle.python.security.TaintTracking
|
||||
import semmle.python.web.HttpRequest
|
||||
import semmle.python.security.strings.Untrusted
|
||||
|
||||
import Taint
|
||||
|
||||
from
|
||||
Call call, Expr arg, boolean expected_taint, boolean has_taint, string test_res,
|
||||
string taint_string
|
||||
where
|
||||
call.getLocation().getFile().getShortName() = "test.py" and
|
||||
(
|
||||
call.getFunc().(Name).getId() = "ensure_tainted" and
|
||||
expected_taint = true
|
||||
or
|
||||
call.getFunc().(Name).getId() = "ensure_not_tainted" and
|
||||
expected_taint = false
|
||||
) and
|
||||
arg = call.getAnArg() and
|
||||
(
|
||||
not exists(TaintedNode tainted | tainted.getAstNode() = arg) and
|
||||
taint_string = "<NO TAINT>" and
|
||||
has_taint = false
|
||||
or
|
||||
exists(TaintedNode tainted | tainted.getAstNode() = arg |
|
||||
taint_string = tainted.getTaintKind().toString()
|
||||
) and
|
||||
has_taint = true
|
||||
) and
|
||||
if expected_taint = has_taint then test_res = "ok " else test_res = "fail"
|
||||
// if expected_taint = has_taint then test_res = "✓" else test_res = "✕"
|
||||
select arg.getLocation().toString(), test_res, call.getScope().(Function).getName(), arg.toString(),
|
||||
taint_string
|
||||
@@ -0,0 +1 @@
|
||||
semmle-extractor-options: --max-import-depth=2 -p ../../../query-tests/Security/lib/
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Test that shows fabric.api.execute propagates taint"""
|
||||
|
||||
from fabric.api import run, execute
|
||||
|
||||
|
||||
def unsafe(cmd, safe_arg, cmd2=None, safe_optional=5):
|
||||
run('./venv/bin/activate && {}'.format(cmd))
|
||||
ensure_tainted(cmd, cmd2)
|
||||
ensure_not_tainted(safe_arg, safe_optional)
|
||||
|
||||
|
||||
class Foo(object):
|
||||
|
||||
def unsafe(self, cmd, safe_arg, cmd2=None, safe_optional=5):
|
||||
run('./venv/bin/activate && {}'.format(cmd))
|
||||
ensure_tainted(cmd, cmd2)
|
||||
ensure_not_tainted(safe_arg, safe_optional)
|
||||
|
||||
|
||||
def some_http_handler():
|
||||
cmd = TAINTED_STRING
|
||||
cmd2 = TAINTED_STRING
|
||||
ensure_tainted(cmd, cmd2)
|
||||
|
||||
execute(unsafe, cmd=cmd, safe_arg='safe_arg', cmd2=cmd2)
|
||||
|
||||
foo = Foo()
|
||||
execute(foo.unsafe, cmd, 'safe_arg', cmd2)
|
||||
@@ -1,3 +1,3 @@
|
||||
| test.py:41:12:41:18 | Str | This regular expression includes duplicate character 'A' in a set of characters. |
|
||||
| test.py:42:12:42:19 | Str | This regular expression includes duplicate character '0' in a set of characters. |
|
||||
| test.py:43:12:43:21 | Str | This regular expression includes duplicate character '-' in a set of characters. |
|
||||
| test.py:46:12:46:18 | Str | This regular expression includes duplicate character 'A' in a set of characters. |
|
||||
| test.py:47:12:47:19 | Str | This regular expression includes duplicate character '0' in a set of characters. |
|
||||
| test.py:48:12:48:21 | Str | This regular expression includes duplicate character '-' in a set of characters. |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
| test.py:4:12:4:19 | Str | This regular expression includes an unmatchable caret at offset 1. |
|
||||
| test.py:5:12:5:23 | Str | This regular expression includes an unmatchable caret at offset 5. |
|
||||
| test.py:6:12:6:21 | Str | This regular expression includes an unmatchable caret at offset 2. |
|
||||
| test.py:74:12:74:27 | Str | This regular expression includes an unmatchable caret at offset 8. |
|
||||
| test.py:79:12:79:27 | Str | This regular expression includes an unmatchable caret at offset 8. |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
| test.py:29:12:29:19 | Str | This regular expression includes an unmatchable dollar at offset 3. |
|
||||
| test.py:30:12:30:23 | Str | This regular expression includes an unmatchable dollar at offset 3. |
|
||||
| test.py:31:12:31:20 | Str | This regular expression includes an unmatchable dollar at offset 2. |
|
||||
| test.py:75:12:75:26 | Str | This regular expression includes an unmatchable dollar at offset 3. |
|
||||
| test.py:80:12:80:26 | Str | This regular expression includes an unmatchable dollar at offset 3. |
|
||||
|
||||
@@ -30,12 +30,17 @@ re.compile(b"abc$ ")
|
||||
re.compile(b"abc$ (?s)")
|
||||
re.compile(b"\[$] ")
|
||||
|
||||
#Likely false positives for unmatchable dollar
|
||||
re.compile(b"[$] ")
|
||||
re.compile(b"\$ ")
|
||||
re.compile(b"abc$(?m)")
|
||||
re.compile(b"abc$()")
|
||||
|
||||
#Not unmatchable dollar
|
||||
re.match(b"[$] ", b"$ ")
|
||||
re.match(b"\$ ", b"$ ")
|
||||
re.match(b"abc$(?m)", b"abc")
|
||||
re.match(b"abc$()", b"abc")
|
||||
re.match(b"((a$)|b)*", b"bba")
|
||||
re.match(b"((a$)|b){4}", b"bbba") # Inspired by FP report here: https://github.com/github/codeql/issues/2403
|
||||
re.match(b"((a$).*)", b"a")
|
||||
re.match("(\Aab$|\Aba$)$\Z", "ab")
|
||||
re.match(b"((a$\Z)|b){4}", b"bbba")
|
||||
re.match(b"(a){00}b", b"b")
|
||||
|
||||
#Duplicate character in set
|
||||
re.compile(b"[AA]")
|
||||
@@ -139,3 +144,10 @@ re.compile(r'(?:(?P<n1>^(?:|x)))')
|
||||
|
||||
#Potentially mis-parsed character set
|
||||
re.compile(r"\[(?P<txt>[^[]*)\]\((?P<uri>[^)]*)")
|
||||
|
||||
#Allow unicode in raw strings
|
||||
re.compile(r"[\U00010000-\U0010FFFF]")
|
||||
re.compile(r"[\u0000-\uFFFF]")
|
||||
|
||||
#Allow unicode names
|
||||
re.compile(r"[\N{degree sign}\N{EM DASH}]")
|
||||
@@ -17,3 +17,7 @@ def safe(request):
|
||||
target = request.args.get('target', '')
|
||||
if SAFE_REGEX.match(target):
|
||||
return redirect(target)
|
||||
|
||||
# FP reported in https://github.com/github/codeql/issues/3712
|
||||
# This does not define a regex (but could be used by other code to do so)
|
||||
escaped = re.escape("https://www.humblebundle.com/home/library")
|
||||
|
||||
@@ -17,16 +17,16 @@ def unsafe2(request):
|
||||
|
||||
|
||||
|
||||
#Simplest and safest approach is to use a white-list
|
||||
#Simplest and safest approach is to use an allowlist
|
||||
|
||||
@app.route('/some/path/good1')
|
||||
def safe1(request):
|
||||
whitelist = [
|
||||
allowlist = [
|
||||
"example.com/home",
|
||||
"example.com/login",
|
||||
]
|
||||
target = request.args.get('target', '')
|
||||
if target in whitelist:
|
||||
if target in allowlist:
|
||||
return redirect(target)
|
||||
|
||||
#More complex example allowing sub-domains.
|
||||
|
||||
@@ -23,3 +23,7 @@ def sudo(command, shell=True, pty=True, combine_stderr=None, user=None,
|
||||
quiet=False, warn_only=False, stdout=None, stderr=None, group=None,
|
||||
timeout=None, shell_escape=None, capture_buffer_size=None):
|
||||
pass
|
||||
|
||||
# https://github.com/fabric/fabric/blob/1.14/fabric/tasks.py#L281
|
||||
def execute(task, *args, **kwargs):
|
||||
pass
|
||||
|
||||
10
python/ql/test/query-tests/Security/lib/libxml2/__init__.py
Normal file
10
python/ql/test/query-tests/Security/lib/libxml2/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
def parseFile(filename):
|
||||
return xmlDoc(_obj=None)
|
||||
|
||||
|
||||
class xmlDoc(Object):
|
||||
def __init__(self, _obj=None):
|
||||
pass
|
||||
|
||||
def xpathEval(self, expr):
|
||||
pass
|
||||
@@ -11,7 +11,7 @@ class ETXPath(object):
|
||||
pass
|
||||
|
||||
|
||||
class Xpath(object):
|
||||
class XPath(object):
|
||||
def __init__(self, path, namespaces=None, extensions=None, regexp=True, smart_strings=True):
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
analysis/Consistency.ql
|
||||
@@ -1 +0,0 @@
|
||||
analysis/Sanity.ql
|
||||
Reference in New Issue
Block a user