Merge branch 'main' into p--oj-ox-unsafe-deser

This commit is contained in:
Peter Stöckli
2024-01-30 15:33:39 +01:00
committed by GitHub
7378 changed files with 671327 additions and 189513 deletions

View File

@@ -1,3 +1,34 @@
## 0.8.7
No user-facing changes.
## 0.8.6
No user-facing changes.
## 0.8.5
No user-facing changes.
## 0.8.4
No user-facing changes.
## 0.8.3
No user-facing changes.
## 0.8.2
No user-facing changes.
## 0.8.1
### New Queries
* Added a new experimental query, `rb/jwt-empty-secret-or-algorithm`, to detect when application uses an empty secret or weak algorithm.
* Added a new experimental query, `rb/jwt-missing-verification`, to detect when the application does not verify a JWT payload.
## 0.8.0
### Minor Analysis Improvements

View File

@@ -1,5 +1,6 @@
---
category: newQuery
---
## 0.8.1
### New Queries
* Added a new experimental query, `rb/jwt-empty-secret-or-algorithm`, to detect when application uses an empty secret or weak algorithm.
* Added a new experimental query, `rb/jwt-missing-verification`, to detect when the application does not verify a JWT payload.
* Added a new experimental query, `rb/jwt-missing-verification`, to detect when the application does not verify a JWT payload.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.8.0
lastReleaseVersion: 0.8.7

View File

@@ -1,5 +1,5 @@
name: codeql/ruby-queries
version: 0.8.1-dev
version: 0.8.8-dev
groups:
- ruby
- queries

View File

@@ -1,7 +1,6 @@
/**
* @name Successfully extracted files
* @description Lists all files in the source code directory that were extracted
* without encountering an error in the file.
* @name Extracted files
* @description Lists all files in the source code directory that were extracted.
* @kind diagnostic
* @id rb/diagnostics/successfully-extracted-files
* @tags successfully-extracted-files
@@ -11,7 +10,5 @@ import codeql.ruby.AST
import codeql.ruby.Diagnostics
from File f
where
not exists(ExtractionError e | e.getLocation().getFile() = f) and
exists(f.getRelativePath())
where exists(f.getRelativePath())
select f, ""

View File

@@ -0,0 +1,43 @@
/**
* @name Generate flow models
* @description Queries to generate source, sink, summary and type models.
* @kind table
* @id rb/utils/modeleditor/generate-model
* @tags modeleditor generate-model framework-mode
*/
private import internal.Types
private import internal.Summaries
/**
* Holds if `(type2, path)` should be seen as an instance of `type1`.
*/
query predicate typeModel = Types::typeModel/3;
/**
* Holds if the value at `(type, path)` should be seen as a flow
* source of the given `kind`.
*
* The kind `remote` represents a general remote flow source.
*/
query predicate sourceModel(string type, string path, string kind) { none() }
/**
* Holds if the value at `(type, path)` should be seen as a sink
* of the given `kind`.
*/
query predicate sinkModel(string type, string path, string kind) { none() }
/**
* Holds if calls to `(type, path)`, the value referred to by `input`
* can flow to the value referred to by `output`.
*
* `kind` should be either `value` or `taint`, for value-preserving or taint-preserving steps,
* respectively.
*/
query predicate summaryModel = Summaries::summaryModel/5;
/**
* Holds if `path` can be substituted for a token `TypeVar[name]`.
*/
query predicate typeVariableModel(string name, string path) { none() }

View File

@@ -0,0 +1,71 @@
/**
* Contains predicates for generating `summaryModel`s to summarize flow through methods.
*/
private import ruby
private import codeql.ruby.ApiGraphs
private import codeql.ruby.TaintTracking
private import Util as Util
/**
* Contains predicates for generating `summaryModel`s to summarize flow through methods.
*/
module Summaries {
private module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(DataFlow::MethodNode methodNode | methodNode.isPublic() |
Util::getAnyParameter(methodNode) = source
)
}
predicate isSink(DataFlow::Node sink) { sink = any(DataFlow::MethodNode m).getAReturnNode() }
DataFlow::FlowFeature getAFeature() {
result instanceof DataFlow::FeatureEqualSourceSinkCallContext
}
}
private module ValueFlow {
import DataFlow::Global<Config>
predicate summaryModel(string type, string path, string input, string output) {
exists(DataFlow::MethodNode methodNode, DataFlow::ParameterNode paramNode |
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
flow(paramNode, methodNode.getAReturnNode())
|
Util::pathToMethod(methodNode, type, path) and
input = Util::getArgumentPath(paramNode) and
output = "ReturnValue"
)
}
}
private module TaintFlow {
import TaintTracking::Global<Config>
predicate summaryModel(string type, string path, string input, string output) {
not ValueFlow::summaryModel(type, path, input, output) and
exists(DataFlow::MethodNode methodNode, DataFlow::ParameterNode paramNode |
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
flow(paramNode, methodNode.getAReturnNode())
|
Util::pathToMethod(methodNode, type, path) and
input = Util::getArgumentPath(paramNode) and
output = "ReturnValue"
)
}
}
/**
* Holds if in calls to `(type, path)`, the value referred to by `input`
* can flow to the value referred to by `output`.
*
* `kind` should be either `value` or `taint`, for value-preserving or taint-preserving steps,
* respectively.
*/
predicate summaryModel(string type, string path, string input, string output, string kind) {
ValueFlow::summaryModel(type, path, input, output) and kind = "value"
or
TaintFlow::summaryModel(type, path, input, output) and kind = "taint"
}
}

View File

@@ -0,0 +1,56 @@
/**
* Contains predicates for generating `typeModel`s that contain typing
* information for API nodes.
*/
private import ruby
private import codeql.ruby.ApiGraphs
private import Util as Util
private import codeql.ruby.ast.Module
private import codeql.ruby.ast.internal.Module
/**
* Contains predicates for generating `typeModel`s that contain typing
* information for API nodes.
*/
module Types {
/**
* Holds if `node` should be seen as having the given `type`.
*/
private predicate valueHasTypeName(DataFlow::LocalSourceNode node, string type) {
node.getLocation().getFile() instanceof Util::RelevantFile and
exists(DataFlow::ModuleNode mod |
(
node = mod.getAnImmediateReference().getAMethodCall("new")
or
node = mod.getAnOwnInstanceSelf()
) and
type = mod.getQualifiedName()
or
(
node = mod.getAnImmediateReference()
or
node = mod.getAnOwnModuleSelf()
) and
type = mod.getQualifiedName() + "!"
)
}
/**
* Holds if `(type2, path)` should be seen as an instance of `type1`.
*/
predicate typeModel(string type1, string type2, string path) {
exists(API::Node node |
valueHasTypeName(node.getAValueReachingSink(), type1) and
Util::pathToNode(node, type2, path, true)
)
or
// class Type2 < Type1
// class Type2; include Type1
exists(Module m1, Module m2 |
m2.getAnImmediateAncestor() = m1 and not m2.isBuiltin() and not m1.isBuiltin()
|
m1.getQualifiedName() = type1 and m2.getQualifiedName() = type2 and path = ""
)
}
}

View File

@@ -0,0 +1,137 @@
/**
* Contains utility methods and classes to assist with generating data extensions models.
*/
private import ruby
private import codeql.ruby.ApiGraphs
/**
* A file that is relevant in the context of library modeling.
*
* In practice, this means a file that is not part of test code.
*/
class RelevantFile extends File {
RelevantFile() { not this.getRelativePath().regexpMatch(".*/?test(case)?s?/.*") }
}
/**
* Gets an access path of an argument corresponding to the given `paramNode`.
*/
string getArgumentPath(DataFlow::ParameterNode paramNode) {
paramNode.getLocation().getFile() instanceof RelevantFile and
exists(string paramSpecifier |
exists(Ast::Parameter param |
param = paramNode.asParameter() and
(
paramSpecifier = param.getPosition().toString()
or
paramSpecifier = param.(Ast::KeywordParameter).getName() + ":"
or
param instanceof Ast::BlockParameter and
paramSpecifier = "block"
)
)
or
paramNode instanceof DataFlow::SelfParameterNode and paramSpecifier = "self"
|
result = "Argument[" + paramSpecifier + "]"
)
}
/**
* Holds if `(type,path)` evaluates to the given method, when evalauted from a client of the current library.
*/
predicate pathToMethod(DataFlow::MethodNode method, string type, string path) {
method.getLocation().getFile() instanceof RelevantFile and
exists(DataFlow::ModuleNode mod, string methodName |
method = mod.getOwnInstanceMethod(methodName) and
if methodName = "initialize"
then (
type = mod.getQualifiedName() + "!" and
path = "Method[new]"
) else (
type = mod.getQualifiedName() and
path = "Method[" + methodName + "]"
)
or
method = mod.getOwnSingletonMethod(methodName) and
type = mod.getQualifiedName() + "!" and
path = "Method[" + methodName + "]"
)
}
/**
* Gets any parameter to `methodNode`. This may be a positional, keyword,
* block, or self parameter.
*/
DataFlow::ParameterNode getAnyParameter(DataFlow::MethodNode methodNode) {
result =
[
methodNode.getParameter(_), methodNode.getKeywordParameter(_), methodNode.getBlockParameter(),
methodNode.getSelfParameter()
]
}
private predicate pathToNodeBase(API::Node node, string type, string path, boolean isOutput) {
exists(DataFlow::MethodNode method, string prevPath | pathToMethod(method, type, prevPath) |
isOutput = true and
node = method.getAReturnNode().backtrack() and
path = prevPath + ".ReturnValue" and
not method.getMethodName() = "initialize" // ignore return value of initialize method
or
isOutput = false and
exists(DataFlow::ParameterNode paramNode |
paramNode = getAnyParameter(method) and
node = paramNode.track()
|
path = prevPath + "." + getArgumentPath(paramNode)
)
)
}
private predicate pathToNodeRec(
API::Node node, string type, string path, boolean isOutput, int pathLength
) {
pathLength < 8 and
(
pathToNodeBase(node, type, path, isOutput) and
pathLength = 1
or
exists(API::Node prevNode, string prevPath, boolean prevIsOutput, int prevPathLength |
pathToNodeRec(prevNode, type, prevPath, prevIsOutput, prevPathLength) and
pathLength = prevPathLength + 1
|
node = prevNode.getAnElement() and
path = prevPath + ".Element" and
isOutput = prevIsOutput
or
node = prevNode.getReturn() and
path = prevPath + ".ReturnValue" and
isOutput = prevIsOutput
or
prevIsOutput = false and
isOutput = true and
(
exists(int n |
node = prevNode.getParameter(n) and
path = prevPath + ".Parameter[" + n + "]"
)
or
exists(string name |
node = prevNode.getKeywordParameter(name) and
path = prevPath + ".Parameter[" + name + ":]"
)
or
node = prevNode.getBlock() and
path = prevPath + ".Parameter[block]"
)
)
)
}
/**
* Holds if `(type,path)` evaluates to a value corresponding to `node`, when evaluated from a client of the current library.
*/
predicate pathToNode(API::Node node, string type, string path, boolean isOutput) {
pathToNodeRec(node, type, path, isOutput, _)
}

View File

@@ -7,7 +7,7 @@
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/ReDoS">ReDoS</a>.</li>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Time_complexity">Time complexity</a>.</li>
<li>James Kirrage, Asiri Rathnayake, Hayo Thielecke:
<a href="http://www.cs.bham.ac.uk/~hxt/research/reg-exp-sec.pdf">Static Analysis for Regular Expression Denial-of-Service Attack</a>.
<a href="https://arxiv.org/abs/1301.0849">Static Analysis for Regular Expression Denial-of-Service Attack</a>.
</li>
</references>
</qhelp>

View File

@@ -23,4 +23,5 @@ where
)
or
operation.getBlockMode().isWeak() and msgPrefix = "The block mode " + operation.getBlockMode()
select operation, msgPrefix + " is broken or weak, and should not be used."
select operation, "$@ is broken or weak, and should not be used.", operation.getInitialization(),
msgPrefix

View File

@@ -0,0 +1,17 @@
/**
* @name Fetch endpoints for use in the model editor (application mode)
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
* @kind table
* @id rb/utils/modeleditor/application-mode-endpoints
* @tags modeleditor endpoints application-mode
*/
import codeql.ruby.AST
// This query is empty as Application Mode is not yet supported for Ruby.
from
Call usage, string package, string type, string name, string parameters, boolean supported,
string namespace, string version, string supportedType, string classification
where none()
select usage, package, namespace, type, name, parameters, supported, namespace, version,
supportedType, classification

View File

@@ -0,0 +1,16 @@
/**
* @name Fetch endpoints for use in the model editor (framework mode)
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
* @kind table
* @id rb/utils/modeleditor/framework-mode-endpoints
* @tags modeleditor endpoints framework-mode
*/
import ruby
import codeql.ruby.AST
import ModelEditor
from Endpoint endpoint
select endpoint, endpoint.getNamespace(), endpoint.getType(), endpoint.getName(),
endpoint.getParameters(), endpoint.getSupportedStatus(), endpoint.getFileName(),
endpoint.getSupportedType()

View File

@@ -0,0 +1,215 @@
/** Provides classes and predicates related to handling APIs for the VS Code extension. */
private import ruby
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.internal.DataFlowPrivate
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import codeql.ruby.frameworks.core.Gem
private import codeql.ruby.frameworks.data.ModelsAsData
private import codeql.ruby.frameworks.data.internal.ApiGraphModelsExtensions
private import queries.modeling.internal.Util as Util
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(DataFlow::MethodNode c) {
c.getLocation().getFile() instanceof TestFile
}
private predicate gemFileStep(Gem::GemSpec gem, Folder folder, int n) {
n = 0 and folder.getAFile() = gem.(File)
or
exists(Folder parent, int m |
gemFileStep(gem, parent, m) and
parent.getAFolder() = folder and
n = m + 1
)
}
/**
* Gets the namespace of an endpoint in `file`.
*/
string getNamespace(File file) {
exists(Folder folder | folder = file.getParentContainer() |
// The nearest gemspec to this endpoint, if one exists
result = min(Gem::GemSpec g, int n | gemFileStep(g, folder, n) | g order by n).getName()
or
not gemFileStep(_, folder, _) and
result = ""
)
}
abstract class Endpoint instanceof DataFlow::Node {
string getNamespace() { result = getNamespace(super.getLocation().getFile()) }
string getFileName() { result = super.getLocation().getFile().getBaseName() }
string toString() { result = super.toString() }
Location getLocation() { result = super.getLocation() }
abstract string getType();
abstract string getName();
abstract string getParameters();
abstract boolean getSupportedStatus();
abstract string getSupportedType();
}
/**
* A callable method or accessor from source code.
*/
class MethodEndpoint extends Endpoint instanceof DataFlow::MethodNode {
MethodEndpoint() {
this.isPublic() and
not isUninteresting(this)
}
DataFlow::MethodNode getNode() { result = this }
override string getName() { result = super.getMethodName() }
/**
* Gets the unbound type name of this endpoint.
*/
override string getType() {
result =
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getName()) = this).getQualifiedName() or
result =
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getName()) = this)
.getQualifiedName() + "!"
}
/**
* 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 block params.
result =
"(" +
concat(string key, string value |
value = any(int i | i.toString() = key | super.asCallable().getParameter(i)).getName()
or
exists(DataFlow::ParameterNode param |
param = super.asCallable().getKeywordParameter(key)
|
value = key + ":"
)
|
value, "," order by key
) + ")"
}
/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() { none() }
/** Holds if this API is a known source. */
pragma[nomagic]
predicate isSource() { this.getNode() instanceof SourceCallable }
/** Holds if this API is a known sink. */
pragma[nomagic]
predicate isSink() { this.getNode() instanceof SinkCallable }
/** Holds if this API is a known neutral. */
pragma[nomagic]
predicate isNeutral() { none() }
/**
* 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 = ""
}
}
string methodClassification(Call method) {
method.getFile() instanceof TestFile and result = "test"
or
not method.getFile() instanceof TestFile and
result = "source"
}
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 callable where there exists a MaD sink model that applies to it.
*/
class SinkCallable extends DataFlow::MethodNode {
SinkCallable() {
exists(string type, string path, string method |
method = path.regexpCapture("(Method\\[[^\\]]+\\]).*", 1) and
Util::pathToMethod(this, type, method) and
sinkModel(type, path, _)
)
}
}
/**
* A callable where there exists a MaD source model that applies to it.
*/
class SourceCallable extends DataFlow::CallableNode {
SourceCallable() {
exists(string type, string path, string method |
method = path.regexpCapture("(Method\\[[^\\]]+\\]).*", 1) and
Util::pathToMethod(this, type, method) and
sourceModel(type, path, _)
)
}
}
/**
* A module defined in source code
*/
class ModuleEndpoint extends Endpoint {
private DataFlow::ModuleNode moduleNode;
ModuleEndpoint() {
this =
min(DataFlow::Node n, Location loc |
n.asExpr().getExpr() = moduleNode.getADeclaration() and
loc = n.getLocation()
|
n order by loc.getFile().getAbsolutePath(), loc.getStartLine(), loc.getStartColumn()
) and
not moduleNode.(Module).isBuiltin() and
not moduleNode.getLocation().getFile() instanceof TestFile
}
DataFlow::ModuleNode getNode() { result = moduleNode }
override string getType() { result = this.getNode().getQualifiedName() }
override string getName() { result = "" }
override string getParameters() { result = "" }
override boolean getSupportedStatus() { result = false }
override string getSupportedType() { result = "" }
}