Merge branch 'main' into stdlib-optparse

This commit is contained in:
yoff
2024-09-24 20:24:00 +02:00
committed by GitHub
8853 changed files with 460851 additions and 108009 deletions

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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")

View File

@@ -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"

View File

@@ -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))

View File

@@ -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.

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
## 1.0.2
No user-facing changes.

View 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.

View 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.

View 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/)

View File

@@ -0,0 +1,3 @@
## 1.0.6
No user-facing changes.

View File

@@ -0,0 +1,3 @@
## 1.0.7
No user-facing changes.

View 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.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 1.0.1
lastReleaseVersion: 2.0.0

View 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" }
}

View 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()
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -1,6 +0,0 @@
/**
* Deprecated. Use `semmle.python.regexp.RegexTreeView` instead.
*/
deprecated import regexp.RegexTreeView as Dep
import Dep

View File

@@ -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

View File

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

View File

@@ -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). */

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }
}
//--------

View File

@@ -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() }
}
/**

View File

@@ -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

View File

@@ -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() }

View File

@@ -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

View File

@@ -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() }
}
}
/**

View File

@@ -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() }
}
}
}

View File

@@ -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 {

View File

@@ -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() }
}
}
}

View File

@@ -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() }

View File

@@ -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() }

View File

@@ -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
*

View 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())
}
}
}

View File

@@ -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() {

View File

@@ -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() }

View File

@@ -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
)
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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() }
}

View File

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

View File

@@ -1,4 +0,0 @@
import python
import semmle.python.dataflow.TaintTracking
abstract deprecated class SqlInjectionSink extends TaintSink { }

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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()]
)
}
}
}

View File

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

View File

@@ -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.
*/

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View 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>

View 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"

View File

@@ -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

View File

@@ -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>

View 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."

View 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

View File

@@ -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")

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* The `py/cors-misconfiguration-with-credentials` query, which finds insecure CORS middleware configurations.

View File

@@ -0,0 +1,3 @@
## 1.0.2
No user-facing changes.

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
## 1.0.4
No user-facing changes.

View 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.

View 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.

View File

@@ -0,0 +1,3 @@
## 1.2.1
No user-facing changes.

View 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.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 1.0.1
lastReleaseVersion: 1.2.2

View File

@@ -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;
}

View File

@@ -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;
}

View 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>

View 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"

View File

@@ -0,0 +1,4 @@
@bp.route("/bad")
def bad():
jk = flask.request.form["jk"]
jk = eval_js(f"{jk} f()")

View 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()")

View 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")

View 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>

View 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."

View 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")

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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."

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"}

View File

@@ -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