mirror of
https://github.com/github/codeql.git
synced 2026-04-26 09:15:12 +02:00
Merge branch 'main' into stdlib-optparse
This commit is contained in:
16
python/extractor/poetry.lock
generated
16
python/extractor/poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
@@ -13,13 +13,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.3"
|
||||
version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
|
||||
{file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -87,13 +87,13 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.2"
|
||||
version = "7.4.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
|
||||
{file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
|
||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -224,4 +224,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "efa2573fc074b2b5334ac81c2ed14a4b9894aacab841973703a7af180c181870"
|
||||
content-hash = "77d4ac980bc0aad555f8a72166242260b14251959db0ded9d055c9f06c36c972"
|
||||
|
||||
@@ -15,6 +15,9 @@ pyyaml = "^6.0.1"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest = "^7.4.2"
|
||||
# packaging 24.0 was wrongly released as supporting Python 3.7, while it actually
|
||||
# didn't. So as long as we support Python 3.7, we need to stick to <24.0.
|
||||
packaging = "<24.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
@@ -373,7 +373,8 @@ def syntax_error_message(exception, unit):
|
||||
return error
|
||||
|
||||
def recursion_error_message(exception, unit):
|
||||
l = Location(file=unit.path)
|
||||
# if unit is a BuiltinModuleExtractable, there will be no path attribute
|
||||
l = Location(file=unit.path) if hasattr(unit, "path") else None
|
||||
return (DiagnosticMessage(Source("py/diagnostics/recursion-error", "Recursion error in Python extractor"), Severity.ERROR)
|
||||
.with_location(l)
|
||||
.text(exception.args[0])
|
||||
@@ -383,7 +384,8 @@ def recursion_error_message(exception, unit):
|
||||
)
|
||||
|
||||
def internal_error_message(exception, unit):
|
||||
l = Location(file=unit.path)
|
||||
# if unit is a BuiltinModuleExtractable, there will be no path attribute
|
||||
l = Location(file=unit.path) if hasattr(unit, "path") else None
|
||||
return (DiagnosticMessage(Source("py/diagnostics/internal-error", "Internal error in Python extractor"), Severity.ERROR)
|
||||
.with_location(l)
|
||||
.text("Internal error")
|
||||
|
||||
@@ -10,7 +10,7 @@ from io import BytesIO
|
||||
|
||||
#Semantic version of extractor.
|
||||
#Update this if any changes are made
|
||||
VERSION = "6.1.1"
|
||||
VERSION = "6.1.2"
|
||||
|
||||
PY_EXTENSIONS = ".py", ".pyw"
|
||||
|
||||
|
||||
@@ -274,16 +274,24 @@ def _extract_loop(proc_id, queue, trap_dir, archive, options, reply_queue, logge
|
||||
# Syntax errors have already been handled in extractor.py
|
||||
reply_queue.put(("FAILURE", unit, None))
|
||||
except RecursionError as ex:
|
||||
error = recursion_error_message(ex, unit)
|
||||
diagnostics_writer.write(error)
|
||||
logger.error("Failed to extract %s: %s", unit, ex)
|
||||
logger.traceback(WARN)
|
||||
try:
|
||||
error = recursion_error_message(ex, unit)
|
||||
diagnostics_writer.write(error)
|
||||
except Exception as ex:
|
||||
logger.warning("Failed to write diagnostics: %s", ex)
|
||||
logger.traceback(WARN)
|
||||
reply_queue.put(("FAILURE", unit, None))
|
||||
except Exception as ex:
|
||||
error = internal_error_message(ex, unit)
|
||||
diagnostics_writer.write(error)
|
||||
logger.error("Failed to extract %s: %s", unit, ex)
|
||||
logger.traceback(WARN)
|
||||
try:
|
||||
error = internal_error_message(ex, unit)
|
||||
diagnostics_writer.write(error)
|
||||
except Exception as ex:
|
||||
logger.warning("Failed to write diagnostics: %s", ex)
|
||||
logger.traceback(WARN)
|
||||
reply_queue.put(("FAILURE", unit, None))
|
||||
else:
|
||||
reply_queue.put(("SUCCESS", unit, None))
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
## 2.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Deleted the deprecated `explorationLimit` predicate from `DataFlow::Configuration`, use `FlowExploration<explorationLimit>` instead.
|
||||
* Deleted the deprecated `semmle.python.RegexTreeView` module, use `semmle.python.regexp.RegexTreeView` instead.
|
||||
* Deleted the deprecated `RegexString` class from `regex.qll`.
|
||||
* Deleted the deprecated `Regex` class, use `RegExp` instead.
|
||||
* Deleted the deprecated `semmle/python/security/SQL.qll` file.
|
||||
* Deleted the deprecated `useSSL` predicates from the LDAP libraries, use `useSsl` instead.
|
||||
|
||||
## 1.0.7
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 1.0.6
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Added support for `DictionaryElement[<key>]` and `DictionaryElementAny` when Customizing Library Models for `sourceModel` (see https://codeql.github.com/docs/codeql-language-guides/customizing-library-models-for-python/)
|
||||
|
||||
## 1.0.4
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Additional modelling to detect direct writes to the `Set-Cookie` header has been added for several web frameworks.
|
||||
|
||||
## 1.0.3
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* A number of Python queries now support sinks defined using data extensions. The format of data extensions for Python has been documented.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The common sanitizer guard `StringConstCompareBarrier` has been renamed to `ConstCompareBarrier` and expanded to cover comparisons with other constant values such as `None`. This may result in fewer false positive results for several queries.
|
||||
3
python/ql/lib/change-notes/released/1.0.2.md
Normal file
3
python/ql/lib/change-notes/released/1.0.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.2
|
||||
|
||||
No user-facing changes.
|
||||
5
python/ql/lib/change-notes/released/1.0.3.md
Normal file
5
python/ql/lib/change-notes/released/1.0.3.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 1.0.3
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* A number of Python queries now support sinks defined using data extensions. The format of data extensions for Python has been documented.
|
||||
5
python/ql/lib/change-notes/released/1.0.4.md
Normal file
5
python/ql/lib/change-notes/released/1.0.4.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 1.0.4
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Additional modelling to detect direct writes to the `Set-Cookie` header has been added for several web frameworks.
|
||||
5
python/ql/lib/change-notes/released/1.0.5.md
Normal file
5
python/ql/lib/change-notes/released/1.0.5.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 1.0.5
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Added support for `DictionaryElement[<key>]` and `DictionaryElementAny` when Customizing Library Models for `sourceModel` (see https://codeql.github.com/docs/codeql-language-guides/customizing-library-models-for-python/)
|
||||
3
python/ql/lib/change-notes/released/1.0.6.md
Normal file
3
python/ql/lib/change-notes/released/1.0.6.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.6
|
||||
|
||||
No user-facing changes.
|
||||
3
python/ql/lib/change-notes/released/1.0.7.md
Normal file
3
python/ql/lib/change-notes/released/1.0.7.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.7
|
||||
|
||||
No user-facing changes.
|
||||
10
python/ql/lib/change-notes/released/2.0.0.md
Normal file
10
python/ql/lib/change-notes/released/2.0.0.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## 2.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Deleted the deprecated `explorationLimit` predicate from `DataFlow::Configuration`, use `FlowExploration<explorationLimit>` instead.
|
||||
* Deleted the deprecated `semmle.python.RegexTreeView` module, use `semmle.python.regexp.RegexTreeView` instead.
|
||||
* Deleted the deprecated `RegexString` class from `regex.qll`.
|
||||
* Deleted the deprecated `Regex` class, use `RegExp` instead.
|
||||
* Deleted the deprecated `semmle/python/security/SQL.qll` file.
|
||||
* Deleted the deprecated `useSSL` predicates from the LDAP libraries, use `useSsl` instead.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 1.0.1
|
||||
lastReleaseVersion: 2.0.0
|
||||
|
||||
246
python/ql/lib/modeling/ModelEditor.qll
Normal file
246
python/ql/lib/modeling/ModelEditor.qll
Normal file
@@ -0,0 +1,246 @@
|
||||
/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import python
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
private import semmle.python.frameworks.data.internal.ApiGraphModelsExtensions
|
||||
private import semmle.python.dataflow.new.internal.DataFlowDispatch as DP
|
||||
private import Util as Util
|
||||
|
||||
/**
|
||||
* An string describing the kind of source code element being modeled.
|
||||
*
|
||||
* See `EndPoint`.
|
||||
*/
|
||||
class EndpointKind extends string {
|
||||
EndpointKind() {
|
||||
this in ["Function", "InstanceMethod", "ClassMethod", "StaticMethod", "InitMethod", "Class"]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An element of the source code to be modeled.
|
||||
*
|
||||
* See `EndPointKind` for the possible kinds of elements.
|
||||
*/
|
||||
abstract class Endpoint instanceof Util::RelevantScope {
|
||||
string namespace;
|
||||
string type;
|
||||
string name;
|
||||
|
||||
Endpoint() {
|
||||
exists(string scopePath, string path, int pathIndex |
|
||||
scopePath = Util::computeScopePath(this) and
|
||||
pathIndex = scopePath.indexOf(".", 0, 0)
|
||||
|
|
||||
namespace = scopePath.prefix(pathIndex) and
|
||||
path = scopePath.suffix(pathIndex + 1) and
|
||||
(
|
||||
exists(int nameIndex | nameIndex = max(path.indexOf(".")) |
|
||||
type = path.prefix(nameIndex) and
|
||||
name = path.suffix(nameIndex + 1)
|
||||
)
|
||||
or
|
||||
not exists(path.indexOf(".")) and
|
||||
type = "" and
|
||||
name = path
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the namespace for this endpoint. This will typically be the package in which it is found. */
|
||||
string getNamespace() { result = namespace }
|
||||
|
||||
/** Gets hte basename of the file where this endpoint is found. */
|
||||
string getFileName() { result = super.getLocation().getFile().getBaseName() }
|
||||
|
||||
/** Gets a string representation of this endpoint. */
|
||||
string toString() { result = super.toString() }
|
||||
|
||||
/** Gets the location of this endpoint. */
|
||||
Location getLocation() { result = super.getLocation() }
|
||||
|
||||
/** Gets the name of the class in which this endpoint is found, or the empty string if it is not found inside a class. */
|
||||
string getClass() { result = type }
|
||||
|
||||
/**
|
||||
* Gets the name of the endpoint if it is not a class, or the empty string if it is a class
|
||||
*
|
||||
* If this endpoint is a class, the class name can be obtained via `getType`.
|
||||
*/
|
||||
string getFunctionName() { result = name }
|
||||
|
||||
/**
|
||||
* Gets a string representation of the parameters of this endpoint.
|
||||
*
|
||||
* The string follows a specific format:
|
||||
* - Normal parameters(where arguments can be passed as either positional or keyword) are listed in order, separated by commas.
|
||||
* - Keyword-only parameters are listed in order, separated by commas, each followed by a colon.
|
||||
* - In the future, positional-only parameters will be listed in order, separated by commas, each followed by a slash.
|
||||
*/
|
||||
abstract string getParameters();
|
||||
|
||||
/**
|
||||
* Gets a boolean that is true iff this endpoint is supported by existing modeling.
|
||||
*
|
||||
* The check only takes Models as Data extension models into account.
|
||||
*/
|
||||
abstract boolean getSupportedStatus();
|
||||
|
||||
/**
|
||||
* Gets a string that describes the type of support detected this endpoint.
|
||||
*
|
||||
* The string can be one of the following:
|
||||
* - "source" if this endpoint is a known source.
|
||||
* - "sink" if this endpoint is a known sink.
|
||||
* - "summary" if this endpoint has a flow summary.
|
||||
* - "neutral" if this endpoint is a known neutral.
|
||||
* - "" if this endpoint is not detected as supported.
|
||||
*/
|
||||
abstract string getSupportedType();
|
||||
|
||||
/** Gets the kind of this endpoint. See `EndPointKind`. */
|
||||
abstract EndpointKind getKind();
|
||||
}
|
||||
|
||||
private predicate sourceModelPath(string type, string path) { sourceModel(type, path, _, _) }
|
||||
|
||||
module FindSourceModel = Util::FindModel<sourceModelPath/2>;
|
||||
|
||||
private predicate sinkModelPath(string type, string path) { sinkModel(type, path, _, _) }
|
||||
|
||||
module FindSinkModel = Util::FindModel<sinkModelPath/2>;
|
||||
|
||||
private predicate summaryModelPath(string type, string path) {
|
||||
summaryModel(type, path, _, _, _, _)
|
||||
}
|
||||
|
||||
module FindSummaryModel = Util::FindModel<summaryModelPath/2>;
|
||||
|
||||
private predicate neutralModelPath(string type, string path) { neutralModel(type, path, _) }
|
||||
|
||||
module FindNeutralModel = Util::FindModel<neutralModelPath/2>;
|
||||
|
||||
/**
|
||||
* A callable function or method from source code.
|
||||
*/
|
||||
class FunctionEndpoint extends Endpoint instanceof Function {
|
||||
/**
|
||||
* Gets the parameter types of this endpoint.
|
||||
*/
|
||||
override string getParameters() {
|
||||
// For now, return the names of positional and keyword parameters. We don't always have type information, so we can't return type names.
|
||||
// We don't yet handle splat params or dict splat params.
|
||||
//
|
||||
// In Python, there are three types of parameters:
|
||||
// 1. Positional-only parameters: These are parameters that can only be passed by position and not by keyword.
|
||||
// 2. Positional-or-keyword parameters: These are parameters that can be passed by position or by keyword.
|
||||
// 3. Keyword-only parameters: These are parameters that can only be passed by keyword.
|
||||
//
|
||||
// The syntax for defining these parameters is as follows:
|
||||
// ```python
|
||||
// def f(a, /, b, *, c):
|
||||
// pass
|
||||
// ```
|
||||
// In this example, `a` is a positional-only parameter, `b` is a positional-or-keyword parameter, and `c` is a keyword-only parameter.
|
||||
//
|
||||
// We handle positional-only parameters by adding a "/" to the parameter name, reminiscient of the syntax above.
|
||||
// Note that we don't yet have information about positional-only parameters.
|
||||
// We handle keyword-only parameters by adding a ":" to the parameter name, to be consistent with the MaD syntax and the other languages.
|
||||
exists(int nrPosOnly, Function f |
|
||||
f = this and
|
||||
nrPosOnly = f.getPositionalParameterCount()
|
||||
|
|
||||
result =
|
||||
"(" +
|
||||
concat(string key, string value |
|
||||
// TODO: Once we have information about positional-only parameters:
|
||||
// Handle positional-only parameters by adding a "/"
|
||||
value = any(int i | i.toString() = key | f.getArgName(i))
|
||||
or
|
||||
exists(Name param | param = f.getAKeywordOnlyArg() |
|
||||
param.getId() = key and
|
||||
value = key + ":"
|
||||
)
|
||||
|
|
||||
value, "," order by key
|
||||
) + ")"
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if this API has a supported summary. */
|
||||
pragma[nomagic]
|
||||
predicate hasSummary() { FindSummaryModel::hasModel(this) }
|
||||
|
||||
/** Holds if this API is a known source. */
|
||||
pragma[nomagic]
|
||||
predicate isSource() { FindSourceModel::hasModel(this) }
|
||||
|
||||
/** Holds if this API is a known sink. */
|
||||
pragma[nomagic]
|
||||
predicate isSink() { FindSinkModel::hasModel(this) }
|
||||
|
||||
/** Holds if this API is a known neutral. */
|
||||
pragma[nomagic]
|
||||
predicate isNeutral() { FindNeutralModel::hasModel(this) }
|
||||
|
||||
/**
|
||||
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
|
||||
* recognized source, sink or neutral or it has a flow summary.
|
||||
*/
|
||||
predicate isSupported() {
|
||||
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
|
||||
}
|
||||
|
||||
override boolean getSupportedStatus() {
|
||||
if this.isSupported() then result = true else result = false
|
||||
}
|
||||
|
||||
override string getSupportedType() {
|
||||
this.isSink() and result = "sink"
|
||||
or
|
||||
this.isSource() and result = "source"
|
||||
or
|
||||
this.hasSummary() and result = "summary"
|
||||
or
|
||||
this.isNeutral() and result = "neutral"
|
||||
or
|
||||
not this.isSupported() and result = ""
|
||||
}
|
||||
|
||||
override EndpointKind getKind() {
|
||||
if this.(Function).isMethod()
|
||||
then
|
||||
result = this.methodKind()
|
||||
or
|
||||
not exists(this.methodKind()) and result = "InstanceMethod"
|
||||
else result = "Function"
|
||||
}
|
||||
|
||||
private EndpointKind methodKind() {
|
||||
this.(Function).isMethod() and
|
||||
(
|
||||
DP::isClassmethod(this) and result = "ClassMethod"
|
||||
or
|
||||
DP::isStaticmethod(this) and result = "StaticMethod"
|
||||
or
|
||||
this.(Function).isInitMethod() and result = "InitMethod"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class from source code.
|
||||
*/
|
||||
class ClassEndpoint extends Endpoint instanceof Class {
|
||||
override string getClass() { result = type + "." + name }
|
||||
|
||||
override string getFunctionName() { result = "" }
|
||||
|
||||
override string getParameters() { result = "" }
|
||||
|
||||
override boolean getSupportedStatus() { result = false }
|
||||
|
||||
override string getSupportedType() { result = "" }
|
||||
|
||||
override EndpointKind getKind() { result = "Class" }
|
||||
}
|
||||
86
python/ql/lib/modeling/Util.qll
Normal file
86
python/ql/lib/modeling/Util.qll
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Contains utility methods and classes to assist with generating data extensions models.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.filters.Tests
|
||||
|
||||
/**
|
||||
* A file that probably contains tests.
|
||||
*/
|
||||
class TestFile extends File {
|
||||
TestFile() {
|
||||
this.getRelativePath().regexpMatch(".*(test|spec|examples).+") and
|
||||
not this.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
|
||||
}
|
||||
}
|
||||
|
||||
/** A class to represent scopes that the user might want to model. */
|
||||
class RelevantScope extends Scope {
|
||||
RelevantScope() {
|
||||
this.isPublic() and
|
||||
not this instanceof TestScope and
|
||||
not this.getLocation().getFile() instanceof TestFile and
|
||||
exists(this.getLocation().getFile().getRelativePath())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dotted path of a scope.
|
||||
*/
|
||||
string computeScopePath(RelevantScope scope) {
|
||||
// base case
|
||||
if scope instanceof Module
|
||||
then
|
||||
scope.(Module).isPackageInit() and
|
||||
result = scope.(Module).getPackageName()
|
||||
or
|
||||
not scope.(Module).isPackageInit() and
|
||||
result = scope.(Module).getName()
|
||||
else
|
||||
//recursive cases
|
||||
if scope instanceof Class or scope instanceof Function
|
||||
then result = computeScopePath(scope.getEnclosingScope()) + "." + scope.getName()
|
||||
else result = "unknown: " + scope.toString()
|
||||
}
|
||||
|
||||
signature predicate modelSig(string type, string path);
|
||||
|
||||
/**
|
||||
* A utility module for finding models of endpoints.
|
||||
*
|
||||
* Chiefly the `hasModel` predicate is used to determine if a scope has a model.
|
||||
*/
|
||||
module FindModel<modelSig/2 model> {
|
||||
/**
|
||||
* Holds if the given scope has a model as identified by the provided predicate `model`.
|
||||
*/
|
||||
predicate hasModel(RelevantScope scope) {
|
||||
exists(string type, string path, string searchPath | model(type, path) |
|
||||
searchPath = possibleMemberPathPrefix(path, scope.getName()) and
|
||||
pathToScope(scope, type, searchPath)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the prefix of `path` that might be a path to `member`
|
||||
*/
|
||||
bindingset[path, member]
|
||||
string possibleMemberPathPrefix(string path, string member) {
|
||||
exists(int index | index = path.indexOf(["Member", "Method"] + "[" + member + "]") |
|
||||
result = path.prefix(index)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(type,path)` identifies `scope`.
|
||||
*/
|
||||
bindingset[type, path]
|
||||
predicate pathToScope(RelevantScope scope, string type, string path) {
|
||||
computeScopePath(scope) =
|
||||
type.replaceAll("!", "") + "." +
|
||||
path.replaceAll("Member[", "").replaceAll("]", "").replaceAll("Instance.", "") +
|
||||
scope.getName()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-all
|
||||
version: 1.0.2-dev
|
||||
version: 2.0.1-dev
|
||||
groups: python
|
||||
dbscheme: semmlecode.python.dbscheme
|
||||
extractor: python
|
||||
|
||||
@@ -1134,6 +1134,54 @@ module Http {
|
||||
}
|
||||
}
|
||||
|
||||
/** A key-value pair in a literal for a bulk header update, considered as a single header update. */
|
||||
private class HeaderBulkWriteDictLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
|
||||
{
|
||||
KeyValuePair item;
|
||||
|
||||
HeaderBulkWriteDictLiteral() {
|
||||
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.getBulkArg()) |
|
||||
item = dict.getAnItem()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
|
||||
|
||||
override predicate nameAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
|
||||
}
|
||||
|
||||
override predicate valueAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
|
||||
}
|
||||
}
|
||||
|
||||
/** A tuple in a list for a bulk header update, considered as a single header update. */
|
||||
private class HeaderBulkWriteListLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
|
||||
{
|
||||
Tuple item;
|
||||
|
||||
HeaderBulkWriteListLiteral() {
|
||||
exists(List list | DataFlow::localFlow(DataFlow::exprNode(list), super.getBulkArg()) |
|
||||
item = list.getAnElt()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getElt(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getElt(1) }
|
||||
|
||||
override predicate nameAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
|
||||
}
|
||||
|
||||
override predicate valueAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sets a cookie in an HTTP response.
|
||||
*
|
||||
@@ -1155,6 +1203,77 @@ module Http {
|
||||
* Gets the argument, if any, specifying the cookie value.
|
||||
*/
|
||||
DataFlow::Node getValueArg() { result = super.getValueArg() }
|
||||
|
||||
/**
|
||||
* Holds if the `Secure` flag of the cookie is known to have a value of `b`.
|
||||
*/
|
||||
predicate hasSecureFlag(boolean b) { super.hasSecureFlag(b) }
|
||||
|
||||
/**
|
||||
* Holds if the `HttpOnly` flag of the cookie is known to have a value of `b`.
|
||||
*/
|
||||
predicate hasHttpOnlyFlag(boolean b) { super.hasHttpOnlyFlag(b) }
|
||||
|
||||
/**
|
||||
* Holds if the `SameSite` attribute of the cookie is known to have a value of `v`.
|
||||
*/
|
||||
predicate hasSameSiteAttribute(CookieWrite::SameSiteValue v) { super.hasSameSiteAttribute(v) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dataflow call node to a method that sets a cookie in an http response,
|
||||
* and has common keyword arguments `secure`, `httponly`, and `samesite` to set the attributes of the cookie.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
*/
|
||||
abstract class SetCookieCall extends CookieWrite::Range, DataFlow::CallCfgNode {
|
||||
override predicate hasSecureFlag(boolean b) {
|
||||
super.hasSecureFlag(b)
|
||||
or
|
||||
exists(DataFlow::Node arg, BooleanLiteral bool | arg = this.getArgByName("secure") |
|
||||
DataFlow::localFlow(DataFlow::exprNode(bool), arg) and
|
||||
b = bool.booleanValue()
|
||||
)
|
||||
or
|
||||
not exists(this.getArgByName("secure")) and
|
||||
not exists(this.getKwargs()) and
|
||||
b = false
|
||||
}
|
||||
|
||||
override predicate hasHttpOnlyFlag(boolean b) {
|
||||
super.hasHttpOnlyFlag(b)
|
||||
or
|
||||
exists(DataFlow::Node arg, BooleanLiteral bool | arg = this.getArgByName("httponly") |
|
||||
DataFlow::localFlow(DataFlow::exprNode(bool), arg) and
|
||||
b = bool.booleanValue()
|
||||
)
|
||||
or
|
||||
not exists(this.getArgByName("httponly")) and
|
||||
not exists(this.getKwargs()) and
|
||||
b = false
|
||||
}
|
||||
|
||||
override predicate hasSameSiteAttribute(CookieWrite::SameSiteValue v) {
|
||||
super.hasSameSiteAttribute(v)
|
||||
or
|
||||
exists(DataFlow::Node arg, StringLiteral str | arg = this.getArgByName("samesite") |
|
||||
DataFlow::localFlow(DataFlow::exprNode(str), arg) and
|
||||
(
|
||||
str.getText().toLowerCase() = "strict" and
|
||||
v instanceof CookieWrite::SameSiteStrict
|
||||
or
|
||||
str.getText().toLowerCase() = "lax" and
|
||||
v instanceof CookieWrite::SameSiteLax
|
||||
or
|
||||
str.getText().toLowerCase() = "none" and
|
||||
v instanceof CookieWrite::SameSiteNone
|
||||
)
|
||||
)
|
||||
or
|
||||
not exists(this.getArgByName("samesite")) and
|
||||
not exists(this.getKwargs()) and
|
||||
v instanceof CookieWrite::SameSiteLax // Lax is the default
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new cookie writes on HTTP responses. */
|
||||
@@ -1183,6 +1302,165 @@ module Http {
|
||||
* Gets the argument, if any, specifying the cookie value.
|
||||
*/
|
||||
abstract DataFlow::Node getValueArg();
|
||||
|
||||
/**
|
||||
* Holds if the `Secure` flag of the cookie is known to have a value of `b`.
|
||||
*/
|
||||
predicate hasSecureFlag(boolean b) {
|
||||
exists(StringLiteral sl |
|
||||
// `sl` is likely a substring of the header
|
||||
TaintTracking::localTaint(DataFlow::exprNode(sl), this.getHeaderArg()) and
|
||||
sl.getText().regexpMatch("(?i).*;\\s*secure(;.*|\\s*)") and
|
||||
b = true
|
||||
or
|
||||
// `sl` is the entire header
|
||||
DataFlow::localFlow(DataFlow::exprNode(sl), this.getHeaderArg()) and
|
||||
not sl.getText().regexpMatch("(?i).*;\\s*secure(;.*|\\s*)") and
|
||||
b = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the `HttpOnly` flag of the cookie is known to have a value of `b`.
|
||||
*/
|
||||
predicate hasHttpOnlyFlag(boolean b) {
|
||||
exists(StringLiteral sl |
|
||||
// `sl` is likely a substring of the header
|
||||
TaintTracking::localTaint(DataFlow::exprNode(sl), this.getHeaderArg()) and
|
||||
sl.getText().regexpMatch("(?i).*;\\s*httponly(;.*|\\s*)") and
|
||||
b = true
|
||||
or
|
||||
// `sl` is the entire header
|
||||
DataFlow::localFlow(DataFlow::exprNode(sl), this.getHeaderArg()) and
|
||||
not sl.getText().regexpMatch("(?i).*;\\s*httponly(;.*|\\s*)") and
|
||||
b = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the `SameSite` flag of the cookie is known to have a value of `v`.
|
||||
*/
|
||||
predicate hasSameSiteAttribute(SameSiteValue v) {
|
||||
exists(StringLiteral sl |
|
||||
// `sl` is likely a substring of the header
|
||||
TaintTracking::localTaint(DataFlow::exprNode(sl), this.getHeaderArg()) and
|
||||
(
|
||||
sl.getText().regexpMatch("(?i).*;\\s*samesite=strict(;.*|\\s*)") and
|
||||
v instanceof SameSiteStrict
|
||||
or
|
||||
sl.getText().regexpMatch("(?i).*;\\s*samesite=lax(;.*|\\s*)") and
|
||||
v instanceof SameSiteLax
|
||||
or
|
||||
sl.getText().regexpMatch("(?i).*;\\s*samesite=none(;.*|\\s*)") and
|
||||
v instanceof SameSiteNone
|
||||
)
|
||||
or
|
||||
// `sl` is the entire header
|
||||
DataFlow::localFlow(DataFlow::exprNode(sl), this.getHeaderArg()) and
|
||||
not sl.getText().regexpMatch("(?i).*;\\s*samesite=(strict|lax|none)(;.*|\\s*)") and
|
||||
v instanceof SameSiteLax // Lax is the default
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private newtype TSameSiteValue =
|
||||
TSameSiteStrict() or
|
||||
TSameSiteLax() or
|
||||
TSameSiteNone()
|
||||
|
||||
/** A possible value for the SameSite attribute of a cookie. */
|
||||
class SameSiteValue extends TSameSiteValue {
|
||||
/** Gets a string representation of this value. */
|
||||
string toString() { none() }
|
||||
}
|
||||
|
||||
/** A `Strict` value of the `SameSite` attribute. */
|
||||
class SameSiteStrict extends SameSiteValue, TSameSiteStrict {
|
||||
override string toString() { result = "Strict" }
|
||||
}
|
||||
|
||||
/** A `Lax` value of the `SameSite` attribute. */
|
||||
class SameSiteLax extends SameSiteValue, TSameSiteLax {
|
||||
override string toString() { result = "Lax" }
|
||||
}
|
||||
|
||||
/** A `None` value of the `SameSite` attribute. */
|
||||
class SameSiteNone extends SameSiteValue, TSameSiteNone {
|
||||
override string toString() { result = "None" }
|
||||
}
|
||||
}
|
||||
|
||||
/** A write to a `Set-Cookie` header that sets a cookie directly. */
|
||||
private class CookieHeaderWrite extends CookieWrite::Range instanceof Http::Server::ResponseHeaderWrite
|
||||
{
|
||||
CookieHeaderWrite() {
|
||||
exists(StringLiteral str |
|
||||
str.getText().toLowerCase() = "set-cookie" and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(Http::Server::ResponseHeaderWrite).getNameArg())
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { none() }
|
||||
|
||||
override DataFlow::Node getHeaderArg() {
|
||||
result = this.(Http::Server::ResponseHeaderWrite).getValueArg()
|
||||
}
|
||||
|
||||
override DataFlow::Node getValueArg() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that enables or disables CORS
|
||||
* in a global manner.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CorsMiddleware::Range` instead.
|
||||
*/
|
||||
class CorsMiddleware extends DataFlow::Node instanceof CorsMiddleware::Range {
|
||||
/**
|
||||
* Gets the string corresponding to the middleware
|
||||
*/
|
||||
string getMiddlewareName() { result = super.getMiddlewareName() }
|
||||
|
||||
/**
|
||||
* Gets the dataflow node corresponding to the allowed CORS origins
|
||||
*/
|
||||
DataFlow::Node getOrigins() { result = super.getOrigins() }
|
||||
|
||||
/**
|
||||
* Gets the boolean value corresponding to if CORS credentials is enabled
|
||||
* (`true`) or disabled (`false`) by this node.
|
||||
*/
|
||||
DataFlow::Node getCredentialsAllowed() { result = super.getCredentialsAllowed() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling new CORS middleware APIs. */
|
||||
module CorsMiddleware {
|
||||
/**
|
||||
* 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 `CorsMiddleware` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets the name corresponding to the middleware
|
||||
*/
|
||||
abstract string getMiddlewareName();
|
||||
|
||||
/**
|
||||
* Gets the strings corresponding to the origins allowed by the cors policy
|
||||
*/
|
||||
abstract DataFlow::Node getOrigins();
|
||||
|
||||
/**
|
||||
* Gets the boolean value corresponding to if CORS credentials is enabled
|
||||
* (`true`) or disabled (`false`) by this node.
|
||||
*/
|
||||
abstract DataFlow::Node getCredentialsAllowed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ private import semmle.python.frameworks.Simplejson
|
||||
private import semmle.python.frameworks.SqlAlchemy
|
||||
private import semmle.python.frameworks.Starlette
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
private import semmle.python.frameworks.Streamlit
|
||||
private import semmle.python.frameworks.Toml
|
||||
private import semmle.python.frameworks.Torch
|
||||
private import semmle.python.frameworks.Tornado
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Deprecated. Use `semmle.python.regexp.RegexTreeView` instead.
|
||||
*/
|
||||
|
||||
deprecated import regexp.RegexTreeView as Dep
|
||||
import Dep
|
||||
@@ -85,9 +85,10 @@ class Scope extends Scope_ {
|
||||
this instanceof Module
|
||||
or
|
||||
exists(Module m | m = this.getEnclosingScope() and m.isPublic() |
|
||||
/* If the module has an __all__, is this in it */
|
||||
// The module is implicitly exported
|
||||
not exists(getAModuleExport(m))
|
||||
or
|
||||
// The module is explicitly exported
|
||||
getAModuleExport(m) = this.getName()
|
||||
)
|
||||
or
|
||||
|
||||
@@ -3,34 +3,45 @@
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
|
||||
private predicate stringConstCompare(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
|
||||
private predicate constCompare(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
|
||||
exists(CompareNode cn | cn = g |
|
||||
exists(StringLiteral str_const, Cmpop op |
|
||||
exists(ImmutableLiteral const, Cmpop op |
|
||||
op = any(Eq eq) and branch = true
|
||||
or
|
||||
op = any(NotEq ne) and branch = false
|
||||
|
|
||||
cn.operands(str_const.getAFlowNode(), op, node)
|
||||
cn.operands(const.getAFlowNode(), op, node)
|
||||
or
|
||||
cn.operands(node, op, str_const.getAFlowNode())
|
||||
cn.operands(node, op, const.getAFlowNode())
|
||||
)
|
||||
or
|
||||
exists(IterableNode str_const_iterable, Cmpop op |
|
||||
exists(NameConstant const, Cmpop op |
|
||||
op = any(Is is_) and branch = true
|
||||
or
|
||||
op = any(IsNot isn) and branch = false
|
||||
|
|
||||
cn.operands(const.getAFlowNode(), op, node)
|
||||
or
|
||||
cn.operands(node, op, const.getAFlowNode())
|
||||
)
|
||||
or
|
||||
exists(IterableNode const_iterable, Cmpop op |
|
||||
op = any(In in_) and branch = true
|
||||
or
|
||||
op = any(NotIn ni) and branch = false
|
||||
|
|
||||
forall(ControlFlowNode elem | elem = str_const_iterable.getAnElement() |
|
||||
elem.getNode() instanceof StringLiteral
|
||||
forall(ControlFlowNode elem | elem = const_iterable.getAnElement() |
|
||||
elem.getNode() instanceof ImmutableLiteral
|
||||
) and
|
||||
cn.operands(node, op, str_const_iterable)
|
||||
cn.operands(node, op, const_iterable)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** A validation of unknown node by comparing with a constant string value. */
|
||||
class StringConstCompareBarrier extends DataFlow::Node {
|
||||
StringConstCompareBarrier() {
|
||||
this = DataFlow::BarrierGuard<stringConstCompare/3>::getABarrierNode()
|
||||
}
|
||||
/** A validation of unknown node by comparing with a constant value. */
|
||||
class ConstCompareBarrier extends DataFlow::Node {
|
||||
ConstCompareBarrier() { this = DataFlow::BarrierGuard<constCompare/3>::getABarrierNode() }
|
||||
}
|
||||
|
||||
/** DEPRECATED: Use ConstCompareBarrier instead. */
|
||||
deprecated class StringConstCompareBarrier = ConstCompareBarrier;
|
||||
|
||||
@@ -344,16 +344,6 @@ abstract class DataFlowCallable extends TDataFlowCallable {
|
||||
|
||||
/** Gets the location of this dataflow callable. */
|
||||
abstract Location getLocation();
|
||||
|
||||
/** Gets a best-effort total ordering. */
|
||||
int totalorder() {
|
||||
this =
|
||||
rank[result](DataFlowCallable c, string file, int startline, int startcolumn |
|
||||
c.getLocation().hasLocationInfo(file, startline, startcolumn, _, _)
|
||||
|
|
||||
c order by file, startline, startcolumn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A callable function. */
|
||||
@@ -829,10 +819,16 @@ Function findFunctionAccordingToMro(Class cls, string name) {
|
||||
result = cls.getAMethod() and
|
||||
result.getName() = name
|
||||
or
|
||||
not cls.getAMethod().getName() = name and
|
||||
not class_has_method(cls, name) and
|
||||
result = findFunctionAccordingToMro(getNextClassInMro(cls), name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Join-order helper for `findFunctionAccordingToMro` and `findFunctionAccordingToMroKnownStartingClass`.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate class_has_method(Class cls, string name) { cls.getAMethod().getName() = name }
|
||||
|
||||
/**
|
||||
* Gets a class that, from an approximated MRO calculation, might be the next class
|
||||
* after `cls` in the MRO for `startingClass`.
|
||||
@@ -860,7 +856,7 @@ private Function findFunctionAccordingToMroKnownStartingClass(
|
||||
result.getName() = name and
|
||||
cls = getADirectSuperclass*(startingClass)
|
||||
or
|
||||
not cls.getAMethod().getName() = name and
|
||||
not class_has_method(cls, name) and
|
||||
result =
|
||||
findFunctionAccordingToMroKnownStartingClass(getNextClassInMroKnownStartingClass(cls,
|
||||
startingClass), startingClass, name)
|
||||
@@ -1429,16 +1425,6 @@ abstract class DataFlowCall extends TDataFlowCall {
|
||||
) {
|
||||
this.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
/** Gets a best-effort total ordering. */
|
||||
int totalorder() {
|
||||
this =
|
||||
rank[result](DataFlowCall c, int startline, int startcolumn |
|
||||
c.hasLocationInfo(_, startline, startcolumn, _, _)
|
||||
|
|
||||
c order by startline, startcolumn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A call found in the program source (as opposed to a synthesised call). */
|
||||
|
||||
@@ -168,14 +168,6 @@ abstract deprecated class Configuration extends string {
|
||||
*/
|
||||
predicate hasFlowToExpr(DataFlowExpr sink) { this.hasFlowTo(exprNode(sink)) }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `FlowExploration<explorationLimit>` instead.
|
||||
*
|
||||
* Gets the exploration limit for `hasPartialFlow` and `hasPartialFlowRev`
|
||||
* measured in approximate number of interprocedural steps.
|
||||
*/
|
||||
deprecated int explorationLimit() { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
@@ -290,15 +282,9 @@ deprecated private module Config implements FullStateConfigSig {
|
||||
|
||||
FlowFeature getAFeature() { result = any(Configuration config).getAFeature() }
|
||||
|
||||
predicate sourceGrouping(Node source, string sourceGroup) {
|
||||
any(Configuration config).sourceGrouping(source, sourceGroup)
|
||||
}
|
||||
|
||||
predicate sinkGrouping(Node sink, string sinkGroup) {
|
||||
any(Configuration config).sinkGrouping(sink, sinkGroup)
|
||||
}
|
||||
|
||||
predicate includeHiddenNodes() { any(Configuration config).includeHiddenNodes() }
|
||||
|
||||
predicate observeDiffInformedIncrementalMode() { none() }
|
||||
}
|
||||
|
||||
deprecated private import Impl<Config> as I
|
||||
|
||||
@@ -168,14 +168,6 @@ abstract deprecated class Configuration extends string {
|
||||
*/
|
||||
predicate hasFlowToExpr(DataFlowExpr sink) { this.hasFlowTo(exprNode(sink)) }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `FlowExploration<explorationLimit>` instead.
|
||||
*
|
||||
* Gets the exploration limit for `hasPartialFlow` and `hasPartialFlowRev`
|
||||
* measured in approximate number of interprocedural steps.
|
||||
*/
|
||||
deprecated int explorationLimit() { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
@@ -290,15 +282,9 @@ deprecated private module Config implements FullStateConfigSig {
|
||||
|
||||
FlowFeature getAFeature() { result = any(Configuration config).getAFeature() }
|
||||
|
||||
predicate sourceGrouping(Node source, string sourceGroup) {
|
||||
any(Configuration config).sourceGrouping(source, sourceGroup)
|
||||
}
|
||||
|
||||
predicate sinkGrouping(Node sink, string sinkGroup) {
|
||||
any(Configuration config).sinkGrouping(sink, sinkGroup)
|
||||
}
|
||||
|
||||
predicate includeHiddenNodes() { any(Configuration config).includeHiddenNodes() }
|
||||
|
||||
predicate observeDiffInformedIncrementalMode() { none() }
|
||||
}
|
||||
|
||||
deprecated private import Impl<Config> as I
|
||||
|
||||
@@ -168,14 +168,6 @@ abstract deprecated class Configuration extends string {
|
||||
*/
|
||||
predicate hasFlowToExpr(DataFlowExpr sink) { this.hasFlowTo(exprNode(sink)) }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `FlowExploration<explorationLimit>` instead.
|
||||
*
|
||||
* Gets the exploration limit for `hasPartialFlow` and `hasPartialFlowRev`
|
||||
* measured in approximate number of interprocedural steps.
|
||||
*/
|
||||
deprecated int explorationLimit() { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
@@ -290,15 +282,9 @@ deprecated private module Config implements FullStateConfigSig {
|
||||
|
||||
FlowFeature getAFeature() { result = any(Configuration config).getAFeature() }
|
||||
|
||||
predicate sourceGrouping(Node source, string sourceGroup) {
|
||||
any(Configuration config).sourceGrouping(source, sourceGroup)
|
||||
}
|
||||
|
||||
predicate sinkGrouping(Node sink, string sinkGroup) {
|
||||
any(Configuration config).sinkGrouping(sink, sinkGroup)
|
||||
}
|
||||
|
||||
predicate includeHiddenNodes() { any(Configuration config).includeHiddenNodes() }
|
||||
|
||||
predicate observeDiffInformedIncrementalMode() { none() }
|
||||
}
|
||||
|
||||
deprecated private import Impl<Config> as I
|
||||
|
||||
@@ -168,14 +168,6 @@ abstract deprecated class Configuration extends string {
|
||||
*/
|
||||
predicate hasFlowToExpr(DataFlowExpr sink) { this.hasFlowTo(exprNode(sink)) }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `FlowExploration<explorationLimit>` instead.
|
||||
*
|
||||
* Gets the exploration limit for `hasPartialFlow` and `hasPartialFlowRev`
|
||||
* measured in approximate number of interprocedural steps.
|
||||
*/
|
||||
deprecated int explorationLimit() { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
@@ -290,15 +282,9 @@ deprecated private module Config implements FullStateConfigSig {
|
||||
|
||||
FlowFeature getAFeature() { result = any(Configuration config).getAFeature() }
|
||||
|
||||
predicate sourceGrouping(Node source, string sourceGroup) {
|
||||
any(Configuration config).sourceGrouping(source, sourceGroup)
|
||||
}
|
||||
|
||||
predicate sinkGrouping(Node sink, string sinkGroup) {
|
||||
any(Configuration config).sinkGrouping(sink, sinkGroup)
|
||||
}
|
||||
|
||||
predicate includeHiddenNodes() { any(Configuration config).includeHiddenNodes() }
|
||||
|
||||
predicate observeDiffInformedIncrementalMode() { none() }
|
||||
}
|
||||
|
||||
deprecated private import Impl<Config> as I
|
||||
|
||||
@@ -534,7 +534,7 @@ newtype TDataFlowType = TAnyFlow()
|
||||
|
||||
class DataFlowType extends TDataFlowType {
|
||||
/** Gets a textual representation of this element. */
|
||||
string toString() { result = "DataFlowType" }
|
||||
string toString() { result = "" }
|
||||
}
|
||||
|
||||
/** A node that performs a type cast. */
|
||||
@@ -578,9 +578,6 @@ DataFlowType getNodeType(Node node) {
|
||||
exists(node)
|
||||
}
|
||||
|
||||
/** Gets a string representation of a type returned by `getErasedRepr`. */
|
||||
string ppReprType(DataFlowType t) { none() }
|
||||
|
||||
//--------
|
||||
// Extra flow
|
||||
//--------
|
||||
@@ -1025,8 +1022,6 @@ class NodeRegion instanceof Unit {
|
||||
string toString() { result = "NodeRegion" }
|
||||
|
||||
predicate contains(Node n) { none() }
|
||||
|
||||
int totalOrder() { result = 1 }
|
||||
}
|
||||
|
||||
//--------
|
||||
|
||||
@@ -219,6 +219,12 @@ class CallCfgNode extends CfgNode, LocalSourceNode {
|
||||
|
||||
/** Gets the data-flow node corresponding to the named argument of the call corresponding to this data-flow node */
|
||||
Node getArgByName(string name) { result.asCfgNode() = node.getArgByName(name) }
|
||||
|
||||
/** Gets the data-flow node corresponding to the first tuple (*) argument of the call corresponding to this data-flow node, if any. */
|
||||
Node getStarArg() { result.asCfgNode() = node.getStarArg() }
|
||||
|
||||
/** Gets the data-flow node corresponding to a dictionary (**) argument of the call corresponding to this data-flow node, if any. */
|
||||
Node getKwargs() { result.asCfgNode() = node.getKwargs() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -278,6 +278,12 @@ module ImportResolution {
|
||||
)
|
||||
}
|
||||
|
||||
/** Join-order helper for `getImmediateModuleReference`. */
|
||||
pragma[nomagic]
|
||||
private predicate module_reference_accesses(DataFlow::AttrRead ar, Module p, string attr_name) {
|
||||
ar.accesses(getModuleReference(p), attr_name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a dataflow node that is an immediate reference to the module `m`.
|
||||
*
|
||||
@@ -294,16 +300,13 @@ module ImportResolution {
|
||||
)
|
||||
or
|
||||
// Reading an attribute on a module may return a submodule (or subpackage).
|
||||
exists(DataFlow::AttrRead ar, Module p, string attr_name |
|
||||
ar.accesses(getModuleReference(p), attr_name) and
|
||||
result = ar
|
||||
|
|
||||
exists(Module p, string attr_name | module_reference_accesses(result, p, attr_name) |
|
||||
m = getModuleFromName(p.getPackageName() + "." + attr_name)
|
||||
)
|
||||
or
|
||||
// This is also true for attributes that come from reexports.
|
||||
exists(Module reexporter, string attr_name |
|
||||
result.(DataFlow::AttrRead).accesses(getModuleReference(reexporter), attr_name) and
|
||||
module_reference_accesses(result, reexporter, attr_name) and
|
||||
module_reexport(reexporter, attr_name, m)
|
||||
)
|
||||
or
|
||||
|
||||
@@ -24,6 +24,8 @@ private module CaptureInput implements Shared::InputSig<Location> {
|
||||
}
|
||||
|
||||
class BasicBlock extends PY::BasicBlock {
|
||||
int length() { result = count(int i | exists(this.getNode(i))) }
|
||||
|
||||
Callable getEnclosingCallable() { result = this.getScope() }
|
||||
|
||||
// Note `PY:BasicBlock` does not have a `getLocation`.
|
||||
@@ -34,6 +36,8 @@ private module CaptureInput implements Shared::InputSig<Location> {
|
||||
Location getLocation() { result = super.getNode(0).getLocation() }
|
||||
}
|
||||
|
||||
class ControlFlowNode = PY::ControlFlowNode;
|
||||
|
||||
BasicBlock getImmediateBasicBlockDominator(BasicBlock bb) { result = bb.getImmediateDominator() }
|
||||
|
||||
BasicBlock getABasicBlockSuccessor(BasicBlock bb) { result = bb.getASuccessor() }
|
||||
|
||||
@@ -664,14 +664,6 @@ module DataFlow {
|
||||
}
|
||||
}
|
||||
|
||||
deprecated private class DataFlowType extends TaintKind {
|
||||
// this only exists to avoid an empty recursion error in the type checker
|
||||
DataFlowType() {
|
||||
this = "Data flow" and
|
||||
1 = 2
|
||||
}
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate dict_construct(ControlFlowNode itemnode, ControlFlowNode dictnode) {
|
||||
dictnode.(DictNode).getAValue() = itemnode
|
||||
|
||||
@@ -653,8 +653,7 @@ module AiohttpWebModel {
|
||||
/**
|
||||
* A call to `set_cookie` on a HTTP Response.
|
||||
*/
|
||||
class AiohttpResponseSetCookieCall extends Http::Server::CookieWrite::Range, DataFlow::CallCfgNode
|
||||
{
|
||||
class AiohttpResponseSetCookieCall extends Http::Server::SetCookieCall {
|
||||
AiohttpResponseSetCookieCall() {
|
||||
this = aiohttpResponseInstance().getMember("set_cookie").getACall()
|
||||
}
|
||||
@@ -706,6 +705,33 @@ module AiohttpWebModel {
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dict-like write to an item of the `headers` attribute on a HTTP response, such as
|
||||
* `response.headers[name] = value`.
|
||||
*/
|
||||
class AiohttpResponseHeaderSubscriptWrite extends Http::Server::ResponseHeaderWrite::Range {
|
||||
DataFlow::Node index;
|
||||
DataFlow::Node value;
|
||||
|
||||
AiohttpResponseHeaderSubscriptWrite() {
|
||||
exists(API::Node i |
|
||||
value = aiohttpResponseInstance().getMember("headers").getSubscriptAt(i).asSink() and
|
||||
index = i.asSink() and
|
||||
// To give `this` a value, we need to choose between either LHS or RHS,
|
||||
// and just go with the RHS as it is readily available
|
||||
this = value
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = index }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2170,7 +2170,7 @@ module PrivateDjango {
|
||||
/**
|
||||
* A call to `set_cookie` on a HTTP Response.
|
||||
*/
|
||||
class DjangoResponseSetCookieCall extends Http::Server::CookieWrite::Range,
|
||||
class DjangoResponseSetCookieCall extends Http::Server::SetCookieCall,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
DjangoResponseSetCookieCall() {
|
||||
@@ -2239,6 +2239,71 @@ module PrivateDjango {
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dict-like write to an item of the `headers` attribute on a HTTP response, such as
|
||||
* `response.headers[name] = value`.
|
||||
*/
|
||||
class DjangoResponseHeaderSubscriptWrite extends Http::Server::ResponseHeaderWrite::Range {
|
||||
DataFlow::Node index;
|
||||
DataFlow::Node value;
|
||||
|
||||
DjangoResponseHeaderSubscriptWrite() {
|
||||
exists(SubscriptNode subscript, DataFlow::AttrRead headerLookup |
|
||||
// To give `this` a value, we need to choose between either LHS or RHS,
|
||||
// and just go with the LHS
|
||||
this.asCfgNode() = subscript
|
||||
|
|
||||
headerLookup
|
||||
.accesses(DjangoImpl::DjangoHttp::Response::HttpResponse::instance(), "headers") and
|
||||
exists(DataFlow::Node subscriptObj |
|
||||
subscriptObj.asCfgNode() = subscript.getObject()
|
||||
|
|
||||
headerLookup.flowsTo(subscriptObj)
|
||||
) and
|
||||
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
|
||||
index.asCfgNode() = subscript.getIndex()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = index }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dict-like write to an item of an HTTP response, which is treated as a header write,
|
||||
* such as `response[headerName] = value`
|
||||
*/
|
||||
class DjangoResponseSubscriptWrite extends Http::Server::ResponseHeaderWrite::Range {
|
||||
DataFlow::Node index;
|
||||
DataFlow::Node value;
|
||||
|
||||
DjangoResponseSubscriptWrite() {
|
||||
exists(SubscriptNode subscript |
|
||||
// To give `this` a value, we need to choose between either LHS or RHS,
|
||||
// and just go with the LHS
|
||||
this.asCfgNode() = subscript
|
||||
|
|
||||
subscript.getObject() =
|
||||
DjangoImpl::DjangoHttp::Response::HttpResponse::instance().asCfgNode() and
|
||||
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
|
||||
index.asCfgNode() = subscript.getIndex()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = index }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ private module FabricV1 {
|
||||
// -------------------------------------------------------------------------
|
||||
// fabric.api
|
||||
// -------------------------------------------------------------------------
|
||||
/** Gets a reference to the `fabric.api` module. */
|
||||
API::Node api() { result = fabric().getMember("api") }
|
||||
/** Gets a reference to the `fabric.api` module. Also known as `fabric.operations` */
|
||||
API::Node api() { result = fabric().getMember(["api", "operations"]) }
|
||||
|
||||
/** Provides models for the `fabric.api` module */
|
||||
module Api {
|
||||
|
||||
@@ -30,6 +30,51 @@ module FastApi {
|
||||
API::Node instance() { result = cls().getReturn() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `app.add_middleware` adding a generic middleware.
|
||||
*/
|
||||
private class AddMiddlewareCall extends DataFlow::CallCfgNode {
|
||||
AddMiddlewareCall() { this = App::instance().getMember("add_middleware").getACall() }
|
||||
|
||||
/**
|
||||
* Gets the string corresponding to the middleware
|
||||
*/
|
||||
string getMiddlewareName() { result = this.getArg(0).asExpr().(Name).getId() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `app.add_middleware` adding CORSMiddleware.
|
||||
*/
|
||||
class AddCorsMiddlewareCall extends Http::Server::CorsMiddleware::Range, AddMiddlewareCall {
|
||||
/**
|
||||
* Gets the string corresponding to the middleware
|
||||
*/
|
||||
override string getMiddlewareName() { result = this.getArg(0).asExpr().(Name).getId() }
|
||||
|
||||
/**
|
||||
* Gets the dataflow node corresponding to the allowed CORS origins
|
||||
*/
|
||||
override DataFlow::Node getOrigins() { result = this.getArgByName("allow_origins") }
|
||||
|
||||
/**
|
||||
* Gets the boolean value corresponding to if CORS credentials is enabled
|
||||
* (`true`) or disabled (`false`) by this node.
|
||||
*/
|
||||
override DataFlow::Node getCredentialsAllowed() {
|
||||
result = this.getArgByName("allow_credentials")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dataflow node corresponding to the allowed CORS methods
|
||||
*/
|
||||
DataFlow::Node getMethods() { result = this.getArgByName("allow_methods") }
|
||||
|
||||
/**
|
||||
* Gets the dataflow node corresponding to the allowed CORS headers
|
||||
*/
|
||||
DataFlow::Node getHeaders() { result = this.getArgByName("allow_headers") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi.APIRouter` class
|
||||
*
|
||||
@@ -348,7 +393,7 @@ module FastApi {
|
||||
/**
|
||||
* A call to `set_cookie` on a FastAPI Response.
|
||||
*/
|
||||
private class SetCookieCall extends Http::Server::CookieWrite::Range, DataFlow::MethodCallNode {
|
||||
private class SetCookieCall extends Http::Server::SetCookieCall, DataFlow::MethodCallNode {
|
||||
SetCookieCall() { this.calls(instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
@@ -361,28 +406,59 @@ module FastApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `append` on a `headers` of a FastAPI Response, with the `Set-Cookie`
|
||||
* header-key.
|
||||
* A call to `append` on a `headers` of a FastAPI Response.
|
||||
*/
|
||||
private class HeadersAppendCookie extends Http::Server::CookieWrite::Range,
|
||||
private class HeadersAppend extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
HeadersAppendCookie() {
|
||||
exists(DataFlow::AttrRead headers, DataFlow::Node keyArg |
|
||||
HeadersAppend() {
|
||||
exists(DataFlow::AttrRead headers |
|
||||
headers.accesses(instance(), "headers") and
|
||||
this.calls(headers, "append") and
|
||||
keyArg in [this.getArg(0), this.getArgByName("key")] and
|
||||
keyArg.getALocalSource().asExpr().(StringLiteral).getText().toLowerCase() = "set-cookie"
|
||||
this.calls(headers, "append")
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getHeaderArg() {
|
||||
override DataFlow::Node getNameArg() { result = [this.getArg(0), this.getArgByName("key")] }
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { none() }
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override DataFlow::Node getValueArg() { none() }
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dict-like write to an item of the `headers` attribute on a HTTP response, such as
|
||||
* `response.headers[name] = value`.
|
||||
*/
|
||||
class HeaderSubscriptWrite extends Http::Server::ResponseHeaderWrite::Range {
|
||||
DataFlow::Node index;
|
||||
DataFlow::Node value;
|
||||
|
||||
HeaderSubscriptWrite() {
|
||||
exists(SubscriptNode subscript, DataFlow::AttrRead headerLookup |
|
||||
// To give `this` a value, we need to choose between either LHS or RHS,
|
||||
// and just go with the LHS
|
||||
this.asCfgNode() = subscript
|
||||
|
|
||||
headerLookup.accesses(instance(), "headers") and
|
||||
exists(DataFlow::Node subscriptObj | subscriptObj.asCfgNode() = subscript.getObject() |
|
||||
headerLookup.flowsTo(subscriptObj)
|
||||
) and
|
||||
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
|
||||
index.asCfgNode() = subscript.getIndex()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = index }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,9 +583,7 @@ module Flask {
|
||||
*
|
||||
* See https://flask.palletsprojects.com/en/2.0.x/api/#flask.Response.set_cookie
|
||||
*/
|
||||
class FlaskResponseSetCookieCall extends Http::Server::CookieWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
class FlaskResponseSetCookieCall extends Http::Server::SetCookieCall, DataFlow::MethodCallNode {
|
||||
FlaskResponseSetCookieCall() { this.calls(Flask::Response::instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
@@ -255,7 +255,7 @@ module Pyramid {
|
||||
}
|
||||
|
||||
/** A call to `response.set_cookie`. */
|
||||
private class SetCookieCall extends Http::Server::CookieWrite::Range, DataFlow::MethodCallNode {
|
||||
private class SetCookieCall extends Http::Server::SetCookieCall, DataFlow::MethodCallNode {
|
||||
SetCookieCall() { this.calls(instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
@@ -25,6 +25,74 @@ private import semmle.python.frameworks.data.ModelsAsData
|
||||
* - https://www.starlette.io/
|
||||
*/
|
||||
module Starlette {
|
||||
/**
|
||||
* Provides models for the `starlette.app` class
|
||||
*/
|
||||
module App {
|
||||
/** Gets import of `starlette.app`. */
|
||||
API::Node cls() { result = API::moduleImport("starlette").getMember("app") }
|
||||
|
||||
/** Gets a reference to a Starlette application (an instance of `starlette.app`). */
|
||||
API::Node instance() { result = cls().getAnInstance() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to any of the execute methods on a `app.add_middleware`.
|
||||
*/
|
||||
class AddMiddlewareCall extends DataFlow::CallCfgNode {
|
||||
AddMiddlewareCall() {
|
||||
this = [App::instance().getMember("add_middleware").getACall(), Middleware::instance()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string corresponding to the middleware
|
||||
*/
|
||||
string getMiddlewareName() { result = this.getArg(0).asExpr().(Name).getId() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to any of the execute methods on a `app.add_middleware` with CORSMiddleware.
|
||||
*/
|
||||
class AddCorsMiddlewareCall extends AddMiddlewareCall, Http::Server::CorsMiddleware::Range {
|
||||
/**
|
||||
* Gets the string corresponding to the middleware
|
||||
*/
|
||||
override string getMiddlewareName() { result = this.getArg(0).asExpr().(Name).getId() }
|
||||
|
||||
override DataFlow::Node getOrigins() { result = this.getArgByName("allow_origins") }
|
||||
|
||||
override DataFlow::Node getCredentialsAllowed() {
|
||||
result = this.getArgByName("allow_credentials")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dataflow node corresponding to the allowed CORS methods
|
||||
*/
|
||||
DataFlow::Node getMethods() { result = this.getArgByName("allow_methods") }
|
||||
|
||||
/**
|
||||
* Gets the dataflow node corresponding to the allowed CORS headers
|
||||
*/
|
||||
DataFlow::Node getHeaders() { result = this.getArgByName("allow_headers") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `starlette.middleware.Middleware` class
|
||||
*
|
||||
* See https://www.starlette.io/.
|
||||
*/
|
||||
module Middleware {
|
||||
/** Gets a reference to the `starlette.middleware.Middleware` class. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("starlette").getMember("middleware").getMember("Middleware")
|
||||
or
|
||||
result = ModelOutput::getATypeNode("starlette.middleware.Middleware~Subclass").getASubclass*()
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.middleware.Middleware`. */
|
||||
DataFlow::Node instance() { result = classRef().getACall() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `starlette.websockets.WebSocket` class
|
||||
*
|
||||
|
||||
95
python/ql/lib/semmle/python/frameworks/Streamlit.qll
Normal file
95
python/ql/lib/semmle/python/frameworks/Streamlit.qll
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `streamlit` PyPI package.
|
||||
* See https://pypi.org/project/streamlit/.
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.ApiGraphs
|
||||
import semmle.python.Concepts
|
||||
private import semmle.python.frameworks.SqlAlchemy
|
||||
|
||||
/**
|
||||
* Provides models for the `streamlit` PyPI package.
|
||||
* See https://pypi.org/project/streamlit/.
|
||||
*/
|
||||
module Streamlit {
|
||||
/**
|
||||
* The calls to the interactive streamlit widgets, which take untrusted input.
|
||||
*/
|
||||
private class StreamlitInput extends RemoteFlowSource::Range {
|
||||
StreamlitInput() {
|
||||
this =
|
||||
API::moduleImport("streamlit")
|
||||
.getMember(["text_input", "text_area", "chat_input"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "Streamlit user input" }
|
||||
}
|
||||
|
||||
/**
|
||||
* The Streamlit SQLConnection class, which is used to create a connection to a SQL Database.
|
||||
* Streamlit wraps around SQL Alchemy for most database functionality, and adds some on top of it, such as the `query` method.
|
||||
* Streamlit can also connect to Snowflake and Snowpark databases, but the modeling is not the same, so we need to limit the scope to SQL databases.
|
||||
* https://docs.streamlit.io/develop/api-reference/connections/st.connections.sqlconnection#:~:text=to%20data.-,st.connections.SQLConnection,-Streamlit%20Version
|
||||
* We can connect to SQL databases for example with `import streamlit as st; conn = st.connection('pets_db', type='sql')`
|
||||
*/
|
||||
private class StreamlitSqlConnection extends API::CallNode {
|
||||
StreamlitSqlConnection() {
|
||||
exists(StringLiteral str, API::CallNode n |
|
||||
str.getText() = "sql" and
|
||||
n = API::moduleImport("streamlit").getMember("connection").getACall() and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo([n.getArg(1), n.getArgByName("type")]) and
|
||||
this = n
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `query` call that can execute raw queries on a connection to a SQL database.
|
||||
* https://docs.streamlit.io/develop/api-reference/connections/st.connection
|
||||
*/
|
||||
private class QueryMethodCall extends DataFlow::CallCfgNode, SqlExecution::Range {
|
||||
QueryMethodCall() {
|
||||
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("query").getACall())
|
||||
}
|
||||
|
||||
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("sql")] }
|
||||
}
|
||||
|
||||
/**
|
||||
* The Streamlit SQLConnection.connect() call, which returns a a new sqlalchemy.engine.Connection object.
|
||||
* Streamlit creates a connection to a SQL database basing off SQL Alchemy, so we can reuse the models that we already have.
|
||||
*/
|
||||
private class StreamlitSqlAlchemyConnection extends SqlAlchemy::Connection::InstanceSource {
|
||||
StreamlitSqlAlchemyConnection() {
|
||||
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("connect").getACall())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying SQLAlchemy Engine, accessed via `st.connection().engine`.
|
||||
* Streamlit creates an engine to a SQL database basing off SQL Alchemy, so we can reuse the models that we already have.
|
||||
*/
|
||||
private class StreamlitSqlAlchemyEngine extends SqlAlchemy::Engine::InstanceSource {
|
||||
StreamlitSqlAlchemyEngine() {
|
||||
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("engine").asSource())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The SQLAlchemy Session, accessed via `st.connection().session`.
|
||||
* Streamlit can create a session to a SQL database basing off SQL Alchemy, so we can reuse the models that we already have.
|
||||
* For example, the modeling for `session` includes an `execute` method, which is used to execute raw SQL queries.
|
||||
* https://docs.streamlit.io/develop/api-reference/connections/st.connections.sqlconnection#:~:text=SQLConnection.engine-,SQLConnection.session,-Streamlit%20Version
|
||||
*/
|
||||
private class StreamlitSqlSession extends SqlAlchemy::Session::InstanceSource {
|
||||
StreamlitSqlSession() {
|
||||
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("session").asSource())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,50 @@ module Tornado {
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dict-like write to an item of an `HTTPHeaders` object.
|
||||
*/
|
||||
private class TornadoHeaderSubscriptWrite extends Http::Server::ResponseHeaderWrite::Range {
|
||||
DataFlow::Node index;
|
||||
DataFlow::Node value;
|
||||
|
||||
TornadoHeaderSubscriptWrite() {
|
||||
exists(SubscriptNode subscript |
|
||||
subscript.getObject() = instance().asCfgNode() and
|
||||
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
|
||||
index.asCfgNode() = subscript.getIndex() and
|
||||
this.asCfgNode() = subscript
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = index }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `HTTPHeaders.add`.
|
||||
*/
|
||||
private class TornadoHeadersAppendCall extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
TornadoHeadersAppendCall() { this.calls(instance(), "add") }
|
||||
|
||||
override DataFlow::Node getNameArg() { result = [this.getArg(0), this.getArgByName("name")] }
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -209,6 +253,25 @@ module Tornado {
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "request"
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to `RequestHandler.set_header` or `RequestHandler.add_header` */
|
||||
private class TornadoSetHeaderCall extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
TornadoSetHeaderCall() { this.calls(instance(), ["set_header", "add_header"]) }
|
||||
|
||||
override DataFlow::Node getNameArg() {
|
||||
result = [this.getArg(0), this.getArgByName("name")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
|
||||
override predicate nameAllowsNewline() { none() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,7 +592,7 @@ module Tornado {
|
||||
*
|
||||
* See https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.set_cookie
|
||||
*/
|
||||
class TornadoRequestHandlerSetCookieCall extends Http::Server::CookieWrite::Range,
|
||||
class TornadoRequestHandlerSetCookieCall extends Http::Server::SetCookieCall,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
TornadoRequestHandlerSetCookieCall() {
|
||||
|
||||
@@ -235,9 +235,7 @@ private module Twisted {
|
||||
*
|
||||
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.http.Request.html#addCookie
|
||||
*/
|
||||
class TwistedRequestAddCookieCall extends Http::Server::CookieWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
class TwistedRequestAddCookieCall extends Http::Server::SetCookieCall, DataFlow::MethodCallNode {
|
||||
TwistedRequestAddCookieCall() { this.calls(Twisted::Request::instance(), "addCookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
@@ -45,3 +45,25 @@ extensible predicate typeModel(string type1, string type2, string path);
|
||||
* Holds if `path` can be substituted for a token `TypeVar[name]`.
|
||||
*/
|
||||
extensible predicate typeVariableModel(string name, string path);
|
||||
|
||||
/**
|
||||
* Holds if the given extension tuple `madId` should pretty-print as `model`.
|
||||
*
|
||||
* This predicate should only be used in tests.
|
||||
*/
|
||||
predicate interpretModelForTest(QlBuiltins::ExtensionId madId, string model) {
|
||||
exists(string type, string path, string kind |
|
||||
sourceModel(type, path, kind, madId) and
|
||||
model = "Source: " + type + "; " + path + "; " + kind
|
||||
)
|
||||
or
|
||||
exists(string type, string path, string kind |
|
||||
sinkModel(type, path, kind, madId) and
|
||||
model = "Sink: " + type + "; " + path + "; " + kind
|
||||
)
|
||||
or
|
||||
exists(string type, string path, string input, string output, string kind |
|
||||
summaryModel(type, path, input, output, kind, madId) and
|
||||
model = "Summary: " + type + "; " + path + "; " + input + "; " + output + "; " + kind
|
||||
)
|
||||
}
|
||||
|
||||
@@ -134,9 +134,25 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathTokenBase token) {
|
||||
token.getAnArgument() = "any-named" and
|
||||
result = node.getKeywordParameter(_)
|
||||
)
|
||||
or
|
||||
// content based steps
|
||||
//
|
||||
// note: if we want to migrate to use `FlowSummaryImpl::Input::encodeContent` like
|
||||
// they do in Ruby, be aware that we currently don't make
|
||||
// `DataFlow::DictionaryElementContent` just from seeing a subscript read, so we would
|
||||
// need to add that. (also need to handle things like `DictionaryElementAny` which
|
||||
// doesn't have any value for .getAnArgument())
|
||||
(
|
||||
token.getName() = "DictionaryElement" and
|
||||
result = node.getSubscript(token.getAnArgument())
|
||||
or
|
||||
token.getName() = "DictionaryElementAny" and
|
||||
result = node.getASubscript() and
|
||||
not exists(token.getAnArgument())
|
||||
// TODO: ListElement/SetElement/TupleElement
|
||||
)
|
||||
// Some features don't have MaD tokens yet, they would need to be added to API-graphs first.
|
||||
// - decorators ("DecoratedClass", "DecoratedMember", "DecoratedParameter")
|
||||
// - Array/Map elements ("ArrayElement", "Element", "MapKey", "MapValue")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +258,11 @@ InvokeNode getAnInvocationOf(API::Node node) { result = node.getACall() }
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Member", "Instance", "Awaited", "Call", "Method", "Subclass"]
|
||||
name =
|
||||
[
|
||||
"Member", "Instance", "Awaited", "Call", "Method", "Subclass", "DictionaryElement",
|
||||
"DictionaryElementAny"
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,7 +270,7 @@ predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
* in an identifying access path.
|
||||
*/
|
||||
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = ["Instance", "Awaited", "Call", "Subclass"]
|
||||
name = ["Instance", "Awaited", "Call", "Subclass", "DictionaryElementAny"]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +279,7 @@ predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Member", "Method"] and
|
||||
name = ["Member", "Method", "DictionaryElement"] and
|
||||
exists(argument)
|
||||
or
|
||||
name = ["Argument", "Parameter"] and
|
||||
|
||||
@@ -22,9 +22,10 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
/**
|
||||
* Gets the last decorator call for the function `func`, if `func` has decorators.
|
||||
*/
|
||||
private Expr lastDecoratorCall(Function func) {
|
||||
result = func.getDefinition().(FunctionExpr).getADecoratorCall() and
|
||||
not exists(Call other_decorator | other_decorator.getArg(0) = result)
|
||||
pragma[nomagic]
|
||||
private DataFlow::TypeTrackingNode lastDecoratorCall(Function func) {
|
||||
result.asExpr() = func.getDefinition().(FunctionExpr).getADecoratorCall() and
|
||||
not exists(Call other_decorator | other_decorator.getArg(0) = result.asExpr())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,7 @@ private DataFlow::TypeTrackingNode poorMansFunctionTracker(DataFlow::TypeTracker
|
||||
//
|
||||
// Note that this means that we blindly ignore what the decorator actually does to
|
||||
// the function, which seems like an OK tradeoff.
|
||||
result.asExpr() = lastDecoratorCall(func)
|
||||
result = pragma[only_bind_out](lastDecoratorCall(func))
|
||||
)
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = poorMansFunctionTracker(t2, func).track(t2, t))
|
||||
|
||||
@@ -14,8 +14,3 @@ RegExpTerm getTermForExecution(Concepts::RegexExecution exec) {
|
||||
result.isRootTerm()
|
||||
)
|
||||
}
|
||||
|
||||
/** A StringLiteral used as a regular expression */
|
||||
deprecated class RegexString extends Regex {
|
||||
RegexString() { this = RegExpTracking::regExpSource(_).asExpr() }
|
||||
}
|
||||
|
||||
@@ -100,11 +100,6 @@ private module FindRegexMode {
|
||||
private string mode_from_node(DataFlow::Node node) { node = re_flag_tracker(result) }
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `RegExp` instead.
|
||||
*/
|
||||
deprecated class Regex = RegExp;
|
||||
|
||||
/** A StringLiteral used as a regular expression */
|
||||
class RegExp extends Expr instanceof StringLiteral {
|
||||
DataFlow::Node use;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import python
|
||||
import semmle.python.dataflow.TaintTracking
|
||||
|
||||
abstract deprecated class SqlInjectionSink extends TaintSink { }
|
||||
@@ -41,7 +41,9 @@ module CleartextLogging {
|
||||
*/
|
||||
class SensitiveDataSourceAsSource extends Source, SensitiveDataSource {
|
||||
SensitiveDataSourceAsSource() {
|
||||
not SensitiveDataSource.super.getClassification() = SensitiveDataClassification::id()
|
||||
not SensitiveDataSource.super.getClassification() in [
|
||||
SensitiveDataClassification::id(), SensitiveDataClassification::certificate()
|
||||
]
|
||||
}
|
||||
|
||||
override SensitiveDataClassification getClassification() {
|
||||
|
||||
@@ -40,7 +40,9 @@ module CleartextStorage {
|
||||
*/
|
||||
class SensitiveDataSourceAsSource extends Source, SensitiveDataSource {
|
||||
SensitiveDataSourceAsSource() {
|
||||
not SensitiveDataSource.super.getClassification() = SensitiveDataClassification::id()
|
||||
not SensitiveDataSource.super.getClassification() in [
|
||||
SensitiveDataClassification::id(), SensitiveDataClassification::certificate()
|
||||
]
|
||||
}
|
||||
|
||||
override SensitiveDataClassification getClassification() {
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -43,8 +44,15 @@ module CodeInjection {
|
||||
CodeExecutionAsSink() { this = any(CodeExecution e).getCode() }
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("code-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizer extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -78,8 +79,15 @@ module CommandInjection {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("command-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
* "cookie injection"
|
||||
* 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
|
||||
* "cookie injection"
|
||||
* vulnerabilities, as well as extension points for adding your own.
|
||||
*/
|
||||
module CookieInjection {
|
||||
/**
|
||||
* A data flow source for "cookie injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for "cookie injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for "cookie injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A source of remote user input, considered as a flow source.
|
||||
*/
|
||||
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
|
||||
|
||||
/**
|
||||
* A write to a cookie, considered as a sink.
|
||||
*/
|
||||
class CookieWriteSink extends Sink {
|
||||
CookieWriteSink() {
|
||||
exists(Http::Server::CookieWrite cw |
|
||||
this = [cw.getNameArg(), cw.getValueArg(), cw.getHeaderArg()]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Provides a taint-tracking configuration for detecting "cookie injection" vulnerabilities.
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `CookieInjectionFlow` is needed, otherwise
|
||||
* `CookieInjectionCustomizations` should be imported instead.
|
||||
*/
|
||||
|
||||
private import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import CookieInjectionCustomizations::CookieInjection
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting "cookie injection" vulnerabilities.
|
||||
*/
|
||||
module CookieInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
|
||||
}
|
||||
|
||||
/** Global taint-tracking for detecting "cookie injection" vulnerabilities. */
|
||||
module CookieInjectionFlow = TaintTracking::Global<CookieInjectionConfig>;
|
||||
@@ -51,56 +51,6 @@ module HttpHeaderInjection {
|
||||
}
|
||||
}
|
||||
|
||||
/** A key-value pair in a literal for a bulk header update, considered as a single header update. */
|
||||
// TODO: We could instead consider bulk writes as sinks with an implicit read step of DictionaryKey/DictionaryValue content as needed.
|
||||
private class HeaderBulkWriteDictLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
|
||||
{
|
||||
KeyValuePair item;
|
||||
|
||||
HeaderBulkWriteDictLiteral() {
|
||||
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.getBulkArg()) |
|
||||
item = dict.getAnItem()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
|
||||
|
||||
override predicate nameAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
|
||||
}
|
||||
|
||||
override predicate valueAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
|
||||
}
|
||||
}
|
||||
|
||||
/** A tuple in a list for a bulk header update, considered as a single header update. */
|
||||
// TODO: We could instead consider bulk writes as sinks with implicit read steps as needed.
|
||||
private class HeaderBulkWriteListLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
|
||||
{
|
||||
Tuple item;
|
||||
|
||||
HeaderBulkWriteListLiteral() {
|
||||
exists(List list | DataFlow::localFlow(DataFlow::exprNode(list), super.getBulkArg()) |
|
||||
item = list.getAnElt()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getElt(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getElt(1) }
|
||||
|
||||
override predicate nameAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
|
||||
}
|
||||
|
||||
override predicate valueAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to replace line breaks, considered as a sanitizer.
|
||||
*/
|
||||
|
||||
@@ -61,15 +61,20 @@ module LdapInjection {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsDnSanitizerGuard extends DnSanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsDnSanitizerGuard extends DnSanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsDnSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsDnSanitizerGuard;
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsFilterSanitizerGuard extends FilterSanitizer, StringConstCompareBarrier {
|
||||
}
|
||||
class ConstCompareAsFilterSanitizerGuard extends FilterSanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsFilterSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsFilterSanitizerGuard = ConstCompareAsFilterSanitizerGuard;
|
||||
|
||||
/**
|
||||
* A call to replace line breaks functions as a sanitizer.
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -71,10 +72,17 @@ module LogInjection {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("log-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
|
||||
/**
|
||||
* A call to replace line breaks, considered as a sanitizer.
|
||||
|
||||
@@ -87,7 +87,10 @@ module PathInjection {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,10 @@ module PolynomialReDoS {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ module ReflectedXss {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -72,9 +72,12 @@ module ServerSideRequestForgery {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
|
||||
/**
|
||||
* A string construction (concat, format, f-string) where the left side is not
|
||||
|
||||
@@ -51,9 +51,12 @@ module SqlInjection {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -48,8 +49,15 @@ module UnsafeDeserialization {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("unsafe-deserialization").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -89,6 +90,10 @@ module UrlRedirect {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("url-redirection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The right side of a string-concat, considered as a sanitizer.
|
||||
*/
|
||||
@@ -135,12 +140,15 @@ module UrlRedirect {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier {
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier {
|
||||
override predicate sanitizes(FlowState state) {
|
||||
// sanitize all flow states
|
||||
any()
|
||||
}
|
||||
}
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
## 1.2.2
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* The `py/clear-text-logging-sensitive-data` and `py/clear-text-storage-sensitive-data` queries have been updated to exclude the `certificate` classification of sensitive sources, which often do not contain sensitive data.
|
||||
|
||||
## 1.2.1
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### New Queries
|
||||
|
||||
* The `py/cookie-injection` query, originally contributed to the experimental query pack by @jorgectf, has been promoted to the main query pack. This query finds instances of cookies being set without the `Secure`, `HttpOnly`, or `SameSite` attributes set to secure values.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### New Queries
|
||||
|
||||
* The `py/cookie-injection` query, originally contributed to the experimental query pack by @jorgectf, has been promoted to the main query pack. This query finds instances of cookies being constructed from user input.
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Added models of `streamlit` PyPI package.
|
||||
|
||||
## 1.0.4
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 1.0.3
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Adding Python support for Hardcoded Credentials as Models as Data
|
||||
* Additional sanitizers have been added to the `py/full-ssrf` and `py/partial-ssrf` queries for methods that verify a string contains only a certain set of characters, such as `.isalnum()` as well as regular expression tests.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
27
python/ql/src/Security/CWE-020/CookieInjection.qhelp
Normal file
27
python/ql/src/Security/CWE-020/CookieInjection.qhelp
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>Constructing cookies from user input can allow an attacker to control a user's cookie.
|
||||
This may lead to a session fixation attack. Additionally, client code may not expect a cookie to contain attacker-controlled data, and fail to sanitize it for common vulnerabilities such as Cross Site Scripting (XSS).
|
||||
An attacker manipulating the raw cookie header may additionally be able to set cookie attributes such as <code>HttpOnly</code> to insecure values.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>Do not use raw user input to construct cookies.</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>In the following cases, a cookie is constructed for a Flask response using user input. The first uses <code>set_cookie</code>,
|
||||
and the second sets a cookie's raw value through the <code>set-cookie</code> header.</p>
|
||||
<sample src="examples/CookieInjection.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Wikipedia - <a href="https://en.wikipedia.org/wiki/Session_fixation">Session Fixation</a>.</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
20
python/ql/src/Security/CWE-020/CookieInjection.ql
Normal file
20
python/ql/src/Security/CWE-020/CookieInjection.ql
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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 warning
|
||||
* @precision high
|
||||
* @security-severity 5.0
|
||||
* @id py/cookie-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-20
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.security.dataflow.CookieInjectionQuery
|
||||
import CookieInjectionFlow::PathGraph
|
||||
|
||||
from CookieInjectionFlow::PathNode source, CookieInjectionFlow::PathNode sink
|
||||
where CookieInjectionFlow::flowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "Cookie is constructed from a $@.", source.getNode(),
|
||||
"user-supplied input"
|
||||
@@ -2,15 +2,15 @@ from flask import request, make_response
|
||||
|
||||
|
||||
@app.route("/1")
|
||||
def true():
|
||||
def set_cookie():
|
||||
resp = make_response()
|
||||
resp.set_cookie(request.args["name"],
|
||||
resp.set_cookie(request.args["name"], # BAD: User input is used to set the cookie's name and value
|
||||
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']};"
|
||||
def set_cookie_header():
|
||||
resp = make_response()
|
||||
resp.headers['Set-Cookie'] = f"{request.args['name']}={request.args['name']};" # BAD: User input is used to set the raw cookie header.
|
||||
return resp
|
||||
@@ -4,10 +4,9 @@
|
||||
<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>
|
||||
<p>Cookies without the <code>Secure</code> flag set may be transmittd using HTTP instead of HTTPS, which leaves it vulnerable to being read by a third party.</p>
|
||||
<p>Cookies without the <code>HttpOnly</code> flag set are accessible to JavaScript running in the same origin. In case of a Cross-Site Scripting (XSS) vulnerability, the cookie can be stolen by a malicious script.</p>
|
||||
<p>Cookies with the <code>SameSite</code> attribute set to <code>'None'</code> will be sent with cross-origin requests, which can be controlled by third-party JavaScript code and allow for Cross-Site Request Forgery (CSRF) attacks.</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
@@ -18,9 +17,8 @@ contexts which may be attacker-controlled.</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" />
|
||||
<p>In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the cases marked BAD they are not set.</p>
|
||||
<sample src="examples/InsecureCookie.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
51
python/ql/src/Security/CWE-614/InsecureCookie.ql
Normal file
51
python/ql/src/Security/CWE-614/InsecureCookie.ql
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @name Failure to use secure cookies
|
||||
* @description Insecure cookies may be sent in cleartext, which makes them vulnerable to
|
||||
* interception.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @security-severity 5.0
|
||||
* @precision high
|
||||
* @id py/insecure-cookie
|
||||
* @tags security
|
||||
* external/cwe/cwe-614
|
||||
* external/cwe/cwe-1004
|
||||
* external/cwe/cwe-1275
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.Concepts
|
||||
|
||||
predicate hasProblem(Http::Server::CookieWrite cookie, string alert, int idx) {
|
||||
cookie.hasSecureFlag(false) and
|
||||
alert = "Secure" and
|
||||
idx = 0
|
||||
or
|
||||
cookie.hasHttpOnlyFlag(false) and
|
||||
alert = "HttpOnly" and
|
||||
idx = 1
|
||||
or
|
||||
cookie.hasSameSiteAttribute(any(Http::Server::CookieWrite::SameSiteNone v)) and
|
||||
alert = "SameSite" and
|
||||
idx = 2
|
||||
}
|
||||
|
||||
predicate hasAlert(Http::Server::CookieWrite cookie, string alert) {
|
||||
exists(int numProblems | numProblems = strictcount(string p | hasProblem(cookie, p, _)) |
|
||||
numProblems = 1 and
|
||||
alert = any(string prob | hasProblem(cookie, prob, _)) + " attribute"
|
||||
or
|
||||
numProblems = 2 and
|
||||
alert =
|
||||
strictconcat(string prob, int idx | hasProblem(cookie, prob, idx) | prob, " and " order by idx)
|
||||
+ " attributes"
|
||||
or
|
||||
numProblems = 3 and
|
||||
alert = "Secure, HttpOnly, and SameSite attributes"
|
||||
)
|
||||
}
|
||||
|
||||
from Http::Server::CookieWrite cookie, string alert
|
||||
where hasAlert(cookie, alert)
|
||||
select cookie, "Cookie is added without the " + alert + " properly set."
|
||||
20
python/ql/src/Security/CWE-614/examples/InsecureCookie.py
Normal file
20
python/ql/src/Security/CWE-614/examples/InsecureCookie.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from flask import Flask, request, make_response, Response
|
||||
|
||||
|
||||
@app.route("/good1")
|
||||
def good1():
|
||||
resp = make_response()
|
||||
resp.set_cookie("name", value="value", secure=True, httponly=True, samesite='Strict') # GOOD: Attributes are securely set
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/good2")
|
||||
def good2():
|
||||
resp = make_response()
|
||||
resp.headers['Set-Cookie'] = "name=value; Secure; HttpOnly; SameSite=Strict" # GOOD: Attributes are securely set
|
||||
return resp
|
||||
|
||||
@app.route("/bad1")
|
||||
resp = make_response()
|
||||
resp.set_cookie("name", value="value", samesite='None') # BAD: the SameSite attribute is set to 'None' and the 'Secure' and 'HttpOnly' attributes are set to False by default.
|
||||
return resp
|
||||
@@ -18,6 +18,7 @@ import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.filters.Tests
|
||||
private import semmle.python.dataflow.new.internal.DataFlowDispatch as DataFlowDispatch
|
||||
private import semmle.python.dataflow.new.internal.Builtins::Builtins as Builtins
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
bindingset[char, fraction]
|
||||
predicate fewer_characters_than(StringLiteral str, string char, float fraction) {
|
||||
@@ -80,6 +81,11 @@ class HardcodedValueSource extends DataFlow::Node {
|
||||
|
||||
class CredentialSink extends DataFlow::Node {
|
||||
CredentialSink() {
|
||||
exists(string s | s.matches("credentials-%") |
|
||||
// Actual sink-type will be things like `credentials-password` or `credentials-username`
|
||||
this = ModelOutput::getASinkNode(s).asSink()
|
||||
)
|
||||
or
|
||||
exists(string name |
|
||||
name.regexpMatch(getACredentialRegex()) and
|
||||
not name.matches("%file")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* The `py/cors-misconfiguration-with-credentials` query, which finds insecure CORS middleware configurations.
|
||||
3
python/ql/src/change-notes/released/1.0.2.md
Normal file
3
python/ql/src/change-notes/released/1.0.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.2
|
||||
|
||||
No user-facing changes.
|
||||
@@ -1,4 +1,6 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Additional sanitizers have been added to the `py/full-ssrf` and `py/partial-ssrf` queries for methods that verify a string contains only a certain set of characters, such as `.isalnum()` as well as regular expression tests.
|
||||
## 1.0.3
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Adding Python support for Hardcoded Credentials as Models as Data
|
||||
* Additional sanitizers have been added to the `py/full-ssrf` and `py/partial-ssrf` queries for methods that verify a string contains only a certain set of characters, such as `.isalnum()` as well as regular expression tests.
|
||||
3
python/ql/src/change-notes/released/1.0.4.md
Normal file
3
python/ql/src/change-notes/released/1.0.4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.0.4
|
||||
|
||||
No user-facing changes.
|
||||
9
python/ql/src/change-notes/released/1.1.0.md
Normal file
9
python/ql/src/change-notes/released/1.1.0.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 1.1.0
|
||||
|
||||
### New Queries
|
||||
|
||||
* The `py/cookie-injection` query, originally contributed to the experimental query pack by @jorgectf, has been promoted to the main query pack. This query finds instances of cookies being constructed from user input.
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Added models of `streamlit` PyPI package.
|
||||
5
python/ql/src/change-notes/released/1.2.0.md
Normal file
5
python/ql/src/change-notes/released/1.2.0.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 1.2.0
|
||||
|
||||
### New Queries
|
||||
|
||||
* The `py/cookie-injection` query, originally contributed to the experimental query pack by @jorgectf, has been promoted to the main query pack. This query finds instances of cookies being set without the `Secure`, `HttpOnly`, or `SameSite` attributes set to secure values.
|
||||
3
python/ql/src/change-notes/released/1.2.1.md
Normal file
3
python/ql/src/change-notes/released/1.2.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 1.2.1
|
||||
|
||||
No user-facing changes.
|
||||
5
python/ql/src/change-notes/released/1.2.2.md
Normal file
5
python/ql/src/change-notes/released/1.2.2.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 1.2.2
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* The `py/clear-text-logging-sensitive-data` and `py/clear-text-storage-sensitive-data` queries have been updated to exclude the `certificate` classification of sensitive sources, which often do not contain sensitive data.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 1.0.1
|
||||
lastReleaseVersion: 1.2.2
|
||||
|
||||
@@ -45,7 +45,10 @@ module TemplateInjection {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,10 @@ module XsltInjection {
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
* A comparison with a constant, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
|
||||
class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
|
||||
|
||||
/** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
|
||||
deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
|
||||
}
|
||||
|
||||
24
python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp
Normal file
24
python/ql/src/experimental/Security/CWE-094/Js2Py.qhelp
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Passing untrusted inputs to a JavaScript interpreter like `Js2Py` can lead to arbitrary
|
||||
code execution.
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p> This vulnerability can be prevented either by preventing an untrusted user input to flow
|
||||
to an <code>eval_js</code> call. Or, the impact of this vulnerability can be
|
||||
significantly reduced by disabling imports from the interepreted code (note that in a <a
|
||||
href="https://github.com/PiotrDabkowski/Js2Py/issues/45#issuecomment-258724406">
|
||||
comment</a> the author of the library highlights that Js2Py is still insecure with this
|
||||
option).</p>
|
||||
</recommendation>
|
||||
<example>
|
||||
<p>In the example below, the Javascript code being evaluated is controlled by the user and
|
||||
hence leads to arbitrary code execution.</p>
|
||||
<sample src="Js2pyBad.py" />
|
||||
<p>This can be fixed by disabling imports before evaluating the user passed buffer.</p>
|
||||
<sample src="Js2pyGood.py" />
|
||||
</example>
|
||||
</qhelp>
|
||||
38
python/ql/src/experimental/Security/CWE-094/Js2Py.ql
Normal file
38
python/ql/src/experimental/Security/CWE-094/Js2Py.ql
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @name JavaScript code execution.
|
||||
* @description Passing user supplied arguments to a Javascript to Python translation engine such as Js2Py can lead to remote code execution.
|
||||
* @problem.severity error
|
||||
* @security-severity 9.3
|
||||
* @precision high
|
||||
* @kind path-problem
|
||||
* @id py/js2py-rce
|
||||
* @tags security
|
||||
* experimental
|
||||
* external/cwe/cwe-94
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.ApiGraphs
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
import semmle.python.Concepts
|
||||
|
||||
module Js2PyFlowConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node node) {
|
||||
API::moduleImport("js2py").getMember(["eval_js", "eval_js6", "EvalJs"]).getACall().getArg(_) =
|
||||
node
|
||||
}
|
||||
}
|
||||
|
||||
module Js2PyFlow = TaintTracking::Global<Js2PyFlowConfig>;
|
||||
|
||||
import Js2PyFlow::PathGraph
|
||||
|
||||
from Js2PyFlow::PathNode source, Js2PyFlow::PathNode sink
|
||||
where
|
||||
Js2PyFlow::flowPath(source, sink) and
|
||||
not exists(API::moduleImport("js2py").getMember("disable_pyimport").getACall())
|
||||
select sink, source, sink, "This input to Js2Py depends on a $@.", source.getNode(),
|
||||
"user-provided value"
|
||||
4
python/ql/src/experimental/Security/CWE-094/Js2pyBad.py
Normal file
4
python/ql/src/experimental/Security/CWE-094/Js2pyBad.py
Normal file
@@ -0,0 +1,4 @@
|
||||
@bp.route("/bad")
|
||||
def bad():
|
||||
jk = flask.request.form["jk"]
|
||||
jk = eval_js(f"{jk} f()")
|
||||
6
python/ql/src/experimental/Security/CWE-094/Js2pyGood.py
Normal file
6
python/ql/src/experimental/Security/CWE-094/Js2pyGood.py
Normal file
@@ -0,0 +1,6 @@
|
||||
@bp.route("/good")
|
||||
def good():
|
||||
# disable python imports to prevent execution of malicious code
|
||||
js2py.disable_pyimport()
|
||||
jk = flask.request.form["jk"]
|
||||
jk = eval_js(f"{jk} f()")
|
||||
9
python/ql/src/experimental/Security/CWE-346/CorsBad.py
Normal file
9
python/ql/src/experimental/Security/CWE-346/CorsBad.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import cherrypy
|
||||
|
||||
def bad():
|
||||
request = cherrypy.request
|
||||
validCors = "domain.com"
|
||||
if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']:
|
||||
origin = request.headers.get('Origin', None)
|
||||
if origin.startswith(validCors):
|
||||
print("Origin Valid")
|
||||
28
python/ql/src/experimental/Security/CWE-346/CorsBypass.qhelp
Normal file
28
python/ql/src/experimental/Security/CWE-346/CorsBypass.qhelp
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>Cross-origin resource sharing policy may be bypassed due to incorrect checks like the <code>string.startswith</code> call.</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>Use a more stronger check to test for CORS policy bypass.</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>Most Python frameworks provide a mechanism for testing origins and performing CORS checks.
|
||||
For example, consider the code snippet below, <code>origin</code> is compared using a <code>
|
||||
startswith</code> call against a list of whitelisted origins. This check can be bypassed
|
||||
easily by origin like <code>domain.com.baddomain.com</code>
|
||||
</p>
|
||||
<sample src="CorsBad.py" />
|
||||
<p>This can be prevented by comparing the origin in a manner shown below.
|
||||
</p>
|
||||
<sample src="CorsGood.py" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>PortsSwigger : <a href="https://portswigger.net/web-security/cors"></a>Cross-origin resource
|
||||
sharing (CORS)</li>
|
||||
<li>Related CVE: <a href="https://github.com/advisories/GHSA-824x-jcxf-hpfg">CVE-2022-3457</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
97
python/ql/src/experimental/Security/CWE-346/CorsBypass.ql
Normal file
97
python/ql/src/experimental/Security/CWE-346/CorsBypass.ql
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @name Cross Origin Resource Sharing(CORS) Policy Bypass
|
||||
* @description Checking user supplied origin headers using weak comparators like 'string.startswith' may lead to CORS policy bypass.
|
||||
* @kind path-problem
|
||||
* @problem.severity warning
|
||||
* @id py/cors-bypass
|
||||
* @tags security
|
||||
* externa/cwe/CWE-346
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.ApiGraphs
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.Flow
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* Returns true if the control flow node may be useful in the current context.
|
||||
*
|
||||
* Ideally for more completeness, we should alert on every `startswith` call and every remote flow source which gets partailly checked. But, as this can lead to lots of FPs, we apply heuristics to filter some calls. This predicate provides logic for this filteration.
|
||||
*/
|
||||
private predicate maybeInteresting(ControlFlowNode c) {
|
||||
// Check if the name of the variable which calls the function matches the heuristic.
|
||||
// This would typically occur at the sink.
|
||||
// This should deal with cases like
|
||||
// `origin.startswith("bla")`
|
||||
heuristics(c.(CallNode).getFunction().(AttrNode).getObject().(NameNode).getId())
|
||||
or
|
||||
// Check if the name of the variable passed as an argument to the functions matches the heuristic. This would typically occur at the sink.
|
||||
// This should deal with cases like
|
||||
// `bla.startswith(origin)`
|
||||
heuristics(c.(CallNode).getArg(0).(NameNode).getId())
|
||||
or
|
||||
// Check if the value gets written to any interesting variable. This would typically occur at the source.
|
||||
// This should deal with cases like
|
||||
// `origin = request.headers.get('My-custom-header')`
|
||||
exists(Variable v | heuristics(v.getId()) | c.getASuccessor*().getNode() = v.getAStore())
|
||||
}
|
||||
|
||||
private class StringStartswithCall extends ControlFlowNode {
|
||||
StringStartswithCall() { this.(CallNode).getFunction().(AttrNode).getName() = "startswith" }
|
||||
}
|
||||
|
||||
bindingset[s]
|
||||
predicate heuristics(string s) { s.matches(["%origin%", "%cors%"]) }
|
||||
|
||||
/**
|
||||
* A member of the `cherrypy.request` class taken as a `RemoteFlowSource`.
|
||||
*/
|
||||
class CherryPyRequest extends RemoteFlowSource::Range {
|
||||
CherryPyRequest() {
|
||||
this =
|
||||
API::moduleImport("cherrypy")
|
||||
.getMember("request")
|
||||
.getMember([
|
||||
"charset", "content_type", "filename", "fp", "name", "params", "headers", "length",
|
||||
])
|
||||
.asSource()
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "cherrypy.request" }
|
||||
}
|
||||
|
||||
module CorsBypassConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node node) {
|
||||
exists(StringStartswithCall s |
|
||||
node.asCfgNode() = s.(CallNode).getArg(0) or
|
||||
node.asCfgNode() = s.(CallNode).getFunction().(AttrNode).getObject()
|
||||
)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
exists(API::CallNode c, API::Node n |
|
||||
n = API::moduleImport("cherrypy").getMember("request").getMember("headers") and
|
||||
c = n.getMember("get").getACall()
|
||||
|
|
||||
c.getReturn().asSource() = node2 and n.asSource() = node1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module CorsFlow = TaintTracking::Global<CorsBypassConfig>;
|
||||
|
||||
import CorsFlow::PathGraph
|
||||
|
||||
from CorsFlow::PathNode source, CorsFlow::PathNode sink
|
||||
where
|
||||
CorsFlow::flowPath(source, sink) and
|
||||
(
|
||||
maybeInteresting(source.getNode().asCfgNode())
|
||||
or
|
||||
maybeInteresting(sink.getNode().asCfgNode())
|
||||
)
|
||||
select sink, source, sink,
|
||||
"Potentially incorrect string comparison which could lead to a CORS bypass."
|
||||
9
python/ql/src/experimental/Security/CWE-346/CorsGood.py
Normal file
9
python/ql/src/experimental/Security/CWE-346/CorsGood.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import cherrypy
|
||||
|
||||
def good():
|
||||
request = cherrypy.request
|
||||
validOrigin = "domain.com"
|
||||
if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']:
|
||||
origin = request.headers.get('Origin', None)
|
||||
if origin == validOrigin:
|
||||
print("Origin Valid")
|
||||
@@ -1,28 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @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
|
||||
* experimental
|
||||
* 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 CookieInjectionFlow::PathGraph
|
||||
|
||||
from CookieInjectionFlow::PathNode source, CookieInjectionFlow::PathNode sink, string insecure
|
||||
where
|
||||
CookieInjectionFlow::flowPath(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"
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* @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
|
||||
* experimental
|
||||
* 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."
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Web browsers, by default, disallow cross-origin resource sharing via direct HTTP requests.
|
||||
Still, to satisfy some needs that arose with the growth of the web, an expedient was created to make exceptions possible.
|
||||
CORS (Cross-origin resource sharing) is a mechanism that allows resources of a web endpoint (let's call it "Peer A")
|
||||
to be accessed from another web page belonging to a different domain ("Peer B").
|
||||
</p>
|
||||
<p>
|
||||
For that to happen, Peer A needs to make available its CORS configuration via special headers on the desired endpoint
|
||||
via the OPTIONS method.
|
||||
</p>
|
||||
<p>
|
||||
This configuration can also allow the inclusion of cookies on the cross-origin request,
|
||||
(i.e. when the <code>Access-Control-Allow-Credentials</code> header is set to true)
|
||||
meaning that Peer B can send a request to Peer A that will include the cookies as if the request was executed by the user.
|
||||
</p>
|
||||
<p>
|
||||
That can have dangerous effects if the origin of Peer B is not restricted correctly.
|
||||
An example of a dangerous scenario is when <code>Access-Control-Allow-Origin</code> header is set to a value obtained from the request made by Peer B
|
||||
(and not correctly validated), or is set to special values such as <code>*</code> or <code>null</code>.
|
||||
The above values can allow any Peer B to send requests to the misconfigured Peer A on behalf of the user.
|
||||
</p>
|
||||
<p>
|
||||
Example scenario:
|
||||
User is client of a bank that has its API misconfigured to accept CORS requests from any domain.
|
||||
When the user loads an evil page, the evil page sends a request to the bank's API to transfer all funds
|
||||
to evil party's account.
|
||||
Given that the user was already logged in to the bank website, and had its session cookies set,
|
||||
the evil party's request succeeds.
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>
|
||||
When configuring CORS that allow credentials passing,
|
||||
it's best not to use user-provided values for the allowed origins response header,
|
||||
especially if the cookies grant session permissions on the user's account.
|
||||
</p>
|
||||
<p>
|
||||
It also can be very dangerous to set the allowed origins to <code>null</code> (which can be bypassed).
|
||||
</p>
|
||||
</recommendation>
|
||||
<example>
|
||||
<p>
|
||||
The first example shows a possible CORS misconfiguration case:
|
||||
</p>
|
||||
<sample src="CorsMisconfigurationMiddlewareBad.py"/>
|
||||
<p>
|
||||
The second example shows a better configuration:
|
||||
</p>
|
||||
<sample src="CorsMisconfigurationMiddlewareGood.py"/>
|
||||
</example>
|
||||
<references>
|
||||
<li>
|
||||
Reference 1: <a href="https://portswigger.net/web-security/cors">PortSwigger Web Security Academy on CORS</a>.
|
||||
</li>
|
||||
<li>
|
||||
Reference 2: <a href="https://www.youtube.com/watch?v=wgkj4ZgxI4c">AppSec EU 2017 Exploiting CORS Misconfigurations For Bitcoins And Bounties by James Kettle</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @name Cors misconfiguration with credentials
|
||||
* @description Disabling or weakening SOP protection may make the application
|
||||
* vulnerable to a CORS attack.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @security-severity 8.8
|
||||
* @precision high
|
||||
* @id py/cors-misconfiguration-with-credentials
|
||||
* @tags security
|
||||
* external/cwe/cwe-942
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
|
||||
predicate containsStar(DataFlow::Node array) {
|
||||
array.asExpr() instanceof List and
|
||||
array.asExpr().getASubExpression().(StringLiteral).getText() in ["*", "null"]
|
||||
or
|
||||
array.asExpr().(StringLiteral).getText() in ["*", "null"]
|
||||
}
|
||||
|
||||
predicate isCorsMiddleware(Http::Server::CorsMiddleware middleware) {
|
||||
middleware.getMiddlewareName() = "CORSMiddleware"
|
||||
}
|
||||
|
||||
predicate credentialsAllowed(Http::Server::CorsMiddleware middleware) {
|
||||
middleware.getCredentialsAllowed().asExpr() instanceof True
|
||||
}
|
||||
|
||||
from Http::Server::CorsMiddleware a
|
||||
where
|
||||
credentialsAllowed(a) and
|
||||
containsStar(a.getOrigins().getALocalSource()) and
|
||||
isCorsMiddleware(a)
|
||||
select a,
|
||||
"This CORS middleware uses a vulnerable configuration that allows arbitrary websites to make authenticated cross-site requests"
|
||||
@@ -0,0 +1,21 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"*"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def main():
|
||||
return {"message": "Hello World"}
|
||||
@@ -0,0 +1,24 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"http://localhost.tiangolo.com",
|
||||
"https://localhost.tiangolo.com",
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def main():
|
||||
return {"message": "Hello World"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user