python: add machinery for MaD barriers

and reinstate previously removed barrier
now as a MaD row
This commit is contained in:
yoff
2025-12-10 01:43:14 +01:00
parent 699ed50432
commit 3dbfb9fa4b
56 changed files with 606 additions and 82 deletions

View File

@@ -927,6 +927,85 @@ module BarrierGuard<guardChecksSig/3 guardChecks> {
}
}
bindingset[this]
private signature class ParamSig;
private module WithParam<ParamSig P> {
/**
* Holds if the guard `g` validates the expression `e` upon evaluating to `branch`.
*
* The expression `e` is expected to be a syntactic part of the guard `g`.
* For example, the guard `g` might be a call `isSafe(x)` and the expression `e`
* the argument `x`.
*/
signature predicate guardChecksSig(CfgNodes::AstCfgNode g, CfgNode e, boolean branch, P param);
}
/**
* Provides a set of barrier nodes for a guard that validates a node.
*
* This is expected to be used in `isBarrier`/`isSanitizer` definitions
* in data flow and taint tracking.
*/
module ParameterizedBarrierGuard<ParamSig P, WithParam<P>::guardChecksSig/4 guardChecks> {
private import codeql.ruby.controlflow.internal.Guards
/**
* Gets an implicit entry definition for a captured variable that
* may be guarded, because a call to the capturing callable is guarded.
*
* This is restricted to calls where the variable is captured inside a
* block.
*/
pragma[nomagic]
private Ssa::CapturedEntryDefinition getAMaybeGuardedCapturedDef(P param) {
exists(
CfgNodes::ExprCfgNode g, boolean branch, CfgNodes::ExprCfgNode testedNode,
Ssa::Definition def, CfgNodes::ExprNodes::CallCfgNode call
|
def.getARead() = testedNode and
guardChecks(g, testedNode, branch, param) and
guardControlsBlock(g, call.getBasicBlock(), branch) and
result.getBasicBlock().getScope() = call.getExpr().(MethodCall).getBlock() and
sameSourceVariable(def, result)
)
}
/** Gets a node that is safely guarded by the given guard check. */
Node getABarrierNode(P param) {
SsaFlow::asNode(result) =
SsaImpl::DataFlowIntegration::ParameterizedBarrierGuard<P, guardChecks/4>::getABarrierNode(param)
or
result.asExpr() = getAMaybeGuardedCapturedDef(param).getARead()
}
}
/**
* Provides a set of barrier nodes for a guard that validates a node as described by an external predicate.
*
* This is expected to be used in `isBarrier`/`isSanitizer` definitions
* in data flow and taint tracking.
*/
module ExternalBarrierGuard {
private import codeql.ruby.frameworks.data.ModelsAsData
private predicate guardCheck(CfgNodes::AstCfgNode g, CfgNode e, boolean branch, string kind) {
// (GuardNode g, ControlFlowNode node, boolean branch, string kind) {
exists(API::Node call, API::Node parameter |
parameter.asSink() = call.asCall().getArgument(_) and
parameter = ModelOutput::getABarrierGuardNode(kind, branch)
|
g = call.asCall().asExpr() and
e = parameter.asSink().asExpr()
)
}
/** Gets a node that is an external barrier of the given kind. */
ExprNode getAnExternalBarrierNode(string kind) {
result = ParameterizedBarrierGuard<string, guardCheck/4>::getABarrierNode(kind)
}
}
/**
* A representation of a run-time module or class.
*

View File

@@ -403,6 +403,33 @@ private module Cached {
predicate getABarrierNode = getABarrierNodeImpl/0;
}
bindingset[this]
private signature class ParamSig;
private module WithParam<ParamSig P> {
signature predicate guardChecksSig(
Cfg::CfgNodes::AstCfgNode g, Cfg::CfgNode e, boolean branch, P param
);
}
overlay[global]
cached // nothing is actually cached
module ParameterizedBarrierGuard<ParamSig P, WithParam<P>::guardChecksSig/4 guardChecks> {
private predicate guardChecksAdjTypes(
DataFlowIntegrationInput::Guard g, DataFlowIntegrationInput::Expr e,
DataFlowIntegrationInput::GuardValue branch, P param
) {
guardChecks(g, e, branch, param)
}
private Node getABarrierNodeImpl(P param) {
result =
DataFlowIntegrationImpl::BarrierGuardWithState<P, guardChecksAdjTypes/4>::getABarrierNode(param)
}
predicate getABarrierNode = getABarrierNodeImpl/1;
}
}
}

View File

@@ -27,7 +27,7 @@ private import codeql.ruby.dataflow.FlowSummary
* A remote flow source originating from a CSV source row.
*/
private class RemoteFlowSourceFromCsv extends RemoteFlowSource::Range {
RemoteFlowSourceFromCsv() { this = ModelOutput::getASourceNode("remote").asSource() }
RemoteFlowSourceFromCsv() { ModelOutput::sourceNode(this, "remote") }
override string getSourceType() { result = "Remote flow (from model)" }
}

View File

@@ -344,6 +344,26 @@ private predicate sinkModel(string type, string path, string kind, string model)
)
}
/** Holds if a barrier model exists for the given parameters. */
private predicate barrierModel(string type, string path, string kind, string model) {
// No deprecation adapter for barrier models, they were not around back then.
exists(QlBuiltins::ExtensionId madId |
Extensions::barrierModel(type, path, kind, madId) and
model = "MaD:" + madId.toString()
)
}
/** Holds if a barrier guard model exists for the given parameters. */
private predicate barrierGuardModel(
string type, string path, string branch, string kind, string model
) {
// No deprecation adapter for barrier models, they were not around back then.
exists(QlBuiltins::ExtensionId madId |
Extensions::barrierGuardModel(type, path, branch, kind, madId) and
model = "MaD:" + madId.toString()
)
}
/** Holds if a summary model `row` exists for the given parameters. */
private predicate summaryModel(
string type, string path, string input, string output, string kind, string model
@@ -400,6 +420,8 @@ predicate isRelevantType(string type) {
(
sourceModel(type, _, _, _) or
sinkModel(type, _, _, _) or
barrierModel(type, _, _, _) or
barrierGuardModel(type, _, _, _, _) or
summaryModel(type, _, _, _, _, _) or
typeModel(_, type, _)
) and
@@ -427,6 +449,8 @@ predicate isRelevantFullPath(string type, string path) {
(
sourceModel(type, path, _, _) or
sinkModel(type, path, _, _) or
barrierModel(type, path, _, _) or
barrierGuardModel(type, path, _, _, _) or
summaryModel(type, path, _, _, _, _) or
typeModel(_, type, path)
)
@@ -747,6 +771,32 @@ module ModelOutput {
)
}
/**
* Holds if a barrier model contributed `barrier` with the given `kind`.
*/
cached
API::Node getABarrierNode(string kind, string model) {
exists(string type, string path |
barrierModel(type, path, kind, model) and
result = getNodeFromPath(type, path)
)
}
/**
* Holds if a barrier model contributed `barrier` with the given `kind` for the given `branch`.
*/
cached
API::Node getABarrierGuardNode(string kind, boolean branch, string model) {
exists(string type, string path, string branch_str |
branch = true and branch_str = "true"
or
branch = false and branch_str = "false"
|
barrierGuardModel(type, path, branch_str, kind, model) and
result = getNodeFromPath(type, path)
)
}
/**
* Holds if a relevant summary exists for these parameters.
*/
@@ -789,15 +839,46 @@ module ModelOutput {
private import codeql.mad.ModelValidation as SharedModelVal
/**
* Holds if a CSV source model contributed `source` with the given `kind`.
* Holds if an external model contributed `source` with the given `kind`.
*/
API::Node getASourceNode(string kind) { result = getASourceNode(kind, _) }
/**
* Holds if a CSV sink model contributed `sink` with the given `kind`.
* Holds if an external model contributed `sink` with the given `kind`.
*/
API::Node getASinkNode(string kind) { result = getASinkNode(kind, _) }
/**
* Holds if an external model contributed `barrier` with the given `kind`.
*/
API::Node getABarrierNode(string kind) { result = getABarrierNode(kind, _) }
/**
* Holds if an external model contributed `barrier-guard` with the given `kind` and `branch`.
*/
API::Node getABarrierGuardNode(string kind, boolean branch) {
result = getABarrierGuardNode(kind, branch, _)
}
/**
* Holds if `node` is specified as a source with the given kind in an external model.
*/
predicate sourceNode(DataFlow::Node node, string kind) { node = getASourceNode(kind).asSource() }
/**
* Holds if `node` is specified as a sink with the given kind in an external model.
*/
predicate sinkNode(DataFlow::Node node, string kind) { node = getASinkNode(kind).asSink() }
/**
* Holds if `node` is specified as a barrier with the given kind in an external model.
*/
predicate barrierNode(DataFlow::Node node, string kind) {
node = getABarrierNode(kind).asSource()
or
node = DataFlow::ExternalBarrierGuard::getAnExternalBarrierNode(kind)
}
private module KindValConfig implements SharedModelVal::KindValidationConfigSig {
predicate summaryKind(string kind) { summaryModel(_, _, _, _, kind, _) }

View File

@@ -20,6 +20,26 @@ extensible predicate sourceModel(
*/
extensible predicate sinkModel(string type, string path, string kind, QlBuiltins::ExtensionId madId);
/**
* Holds if the value at `(type, path)` should be seen as a barrier
* of the given `kind` and `madId` is the data extension row number.
*/
extensible predicate barrierModel(
string type, string path, string kind, QlBuiltins::ExtensionId madId
);
/**
* Holds if the value at `(type, path)` should be seen as a barrier guard
* of the given `kind` and `madId` is the data extension row number.
* `path` is assumed to lead to a parameter of a call (possibly `self`), and
* the call is guarding the parameter.
* `branch` is either `true` or `false`, indicating which branch of the guard
* is protecting the parameter.
*/
extensible predicate barrierGuardModel(
string type, string path, string branch, string kind, QlBuiltins::ExtensionId madId
);
/**
* Holds if in calls to `(type, path)`, the value referred to by `input`
* can flow to the value referred to by `output` and `madId` is the data

View File

@@ -16,6 +16,16 @@ extensions:
extensible: summaryModel
data: []
- addsTo:
pack: codeql/ruby-all
extensible: barrierModel
data: []
- addsTo:
pack: codeql/ruby-all
extensible: barrierGuardModel
data: []
- addsTo:
pack: codeql/ruby-all
extensible: neutralModel

View File

@@ -116,6 +116,6 @@ module CodeInjection {
}
private class ExternalCodeInjectionSink extends Sink {
ExternalCodeInjectionSink() { this = ModelOutput::getASinkNode("code-injection").asSink() }
ExternalCodeInjectionSink() { ModelOutput::sinkNode(this, "code-injection") }
}
}

View File

@@ -55,8 +55,6 @@ module CommandInjection {
}
private class ExternalCommandInjectionSink extends Sink {
ExternalCommandInjectionSink() {
this = ModelOutput::getASinkNode("command-injection").asSink()
}
ExternalCommandInjectionSink() { ModelOutput::sinkNode(this, "command-injection") }
}
}

View File

@@ -38,7 +38,7 @@ class LoggingSink extends Sink {
}
private class ExternalLogInjectionSink extends Sink {
ExternalLogInjectionSink() { this = ModelOutput::getASinkNode("log-injection").asSink() }
ExternalLogInjectionSink() { ModelOutput::sinkNode(this, "log-injection") }
}
/**

View File

@@ -55,6 +55,6 @@ module PathInjection {
{ }
private class ExternalPathInjectionSink extends Sink {
ExternalPathInjectionSink() { this = ModelOutput::getASinkNode("path-injection").asSink() }
ExternalPathInjectionSink() { ModelOutput::sinkNode(this, "path-injection") }
}
}

View File

@@ -44,6 +44,6 @@ module ServerSideRequestForgery {
class StringInterpolationAsSanitizer extends PrefixedStringInterpolation, Sanitizer { }
private class ExternalRequestForgerySink extends Sink {
ExternalRequestForgerySink() { this = ModelOutput::getASinkNode("request-forgery").asSink() }
ExternalRequestForgerySink() { ModelOutput::sinkNode(this, "request-forgery") }
}
}

View File

@@ -59,6 +59,6 @@ module SqlInjection {
private class SqlSanitizationAsSanitizer extends Sanitizer, SqlSanitization { }
private class ExternalSqlInjectionSink extends Sink {
ExternalSqlInjectionSink() { this = ModelOutput::getASinkNode("sql-injection").asSink() }
ExternalSqlInjectionSink() { ModelOutput::sinkNode(this, "sql-injection") }
}
}

View File

@@ -75,7 +75,7 @@ module UrlRedirect {
}
private class ExternalUrlRedirectSink extends Sink {
ExternalUrlRedirectSink() { this = ModelOutput::getASinkNode("url-redirection").asSink() }
ExternalUrlRedirectSink() { ModelOutput::sinkNode(this, "url-redirection") }
}
/**

View File

@@ -80,7 +80,7 @@ module CustomConfig implements DataFlow::ConfigSig {
predicate isSink(DataFlow::Node sink) {
DefaultFlowConfig::isSink(sink)
or
sink = ModelOutput::getASinkNode("test-sink").asSink()
ModelOutput::sinkNode(sink, "test-sink")
}
}