Ruby: Tidy Rails.qll to make adding new settings modeling easier

This commit is contained in:
Alex Ford
2021-12-05 23:17:42 +00:00
parent 737f7332bc
commit 5ce6e63590

View File

@@ -12,6 +12,7 @@ private import codeql.ruby.frameworks.ActiveRecord
private import codeql.ruby.frameworks.ActiveStorage
private import codeql.ruby.ast.internal.Module
private import codeql.ruby.ApiGraphs
private import codeql.ruby.security.CryptoAlgorithms
/**
* A reference to either `Rails::Railtie`, `Rails::Engine`, or `Rails::Application`.
@@ -45,87 +46,143 @@ private DataFlow::CallNode getAConfigureCallNode() {
}
/**
* An access to a Rails config object.
* Classes representing accesses to the Rails config object.
*/
private class ConfigSourceNode extends DataFlow::LocalSourceNode {
ConfigSourceNode() {
// `Foo < Rails::Application ... config ...`
exists(MethodCall configCall | this.asExpr().getExpr() = configCall |
configCall.getMethodName() = "config" and
configCall.getEnclosingModule() instanceof RailtieClass
)
or
// `Rails.application.config`
this =
API::getTopLevelMember("Rails")
.getReturn("application")
.getReturn("config")
.getAnImmediateUse()
or
// `Rails.application.configure { ... config ... }`
// `Rails::Application.configure { ... config ... }`
exists(DataFlow::CallNode configureCallNode, Block block, MethodCall configCall |
configCall = this.asExpr().getExpr()
|
configureCallNode = getAConfigureCallNode() and
block = configureCallNode.asExpr().getExpr().(MethodCall).getBlock() and
configCall.getParent+() = block and
configCall.getMethodName() = "config"
)
private module Config {
/**
* An access to a Rails config object.
*/
private class SourceNode extends DataFlow::LocalSourceNode {
SourceNode() {
// `Foo < Rails::Application ... config ...`
exists(MethodCall configCall | this.asExpr().getExpr() = configCall |
configCall.getMethodName() = "config" and
configCall.getEnclosingModule() instanceof RailtieClass
)
or
// `Rails.application.config`
this =
API::getTopLevelMember("Rails")
.getReturn("application")
.getReturn("config")
.getAnImmediateUse()
or
// `Rails.application.configure { ... config ... }`
// `Rails::Application.configure { ... config ... }`
exists(DataFlow::CallNode configureCallNode, Block block, MethodCall configCall |
configCall = this.asExpr().getExpr()
|
configureCallNode = getAConfigureCallNode() and
block = configureCallNode.asExpr().getExpr().(MethodCall).getBlock() and
configCall.getParent+() = block and
configCall.getMethodName() = "config"
)
}
}
/**
* A reference to the Rails config object.
*/
class Node extends DataFlow::Node {
Node() { exists(SourceNode src | src.flowsTo(this)) }
}
/**
* A reference to the ActionController config object.
*/
class ActionControllerNode extends DataFlow::Node {
ActionControllerNode() {
exists(DataFlow::CallNode source |
source.getReceiver() instanceof Node and
source.getMethodName() = "action_controller"
|
source.flowsTo(this)
)
}
}
/**
* A reference to the ActionDispatch config object.
*/
class ActionDispatchNode extends DataFlow::Node {
ActionDispatchNode() {
exists(DataFlow::CallNode source |
source.getReceiver() instanceof Node and
source.getMethodName() = "action_dispatch"
|
source.flowsTo(this)
)
}
}
}
private class ConfigNode extends DataFlow::Node {
ConfigNode() { exists(ConfigSourceNode src | src.flowsTo(this)) }
}
/**
* Classes representing nodes that set a Rails configuration value.
*/
private module Settings {
private predicate isInTestConfiguration(Location loc) {
loc.getFile().getRelativePath().matches("%test/%") or
loc.getFile().getStem() = "test"
}
// A call where the Rails application config is the receiver
private class CallAgainstConfig extends DataFlow::CallNode {
CallAgainstConfig() { this.getReceiver() instanceof ConfigNode }
MethodCall getCall() { result = this.asExpr().getExpr() }
Block getBlock() { result = this.getCall().getBlock() }
}
private class ActionControllerConfigNode extends DataFlow::Node {
ActionControllerConfigNode() {
exists(CallAgainstConfig source | source.getCall().getMethodName() = "action_controller" |
source.flowsTo(this)
private DataFlow::Node getTransitiveReceiver(DataFlow::CallNode c) {
exists(DataFlow::Node recv |
recv = c.getReceiver() and
(
result = recv
or
recv instanceof DataFlow::CallNode and
result = getTransitiveReceiver(recv)
)
)
}
}
/** Holds if `node` can contain `value`. */
private predicate hasBooleanValue(DataFlow::Node node, boolean value) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().(BooleanLiteral).getValue() = value and
literal.flowsTo(node)
)
}
private class Setting extends DataFlow::CallNode {
Setting() {
// exclude some test configuration
not isInTestConfiguration(this.getLocation()) and
getTransitiveReceiver(this) instanceof Config::Node
}
}
// `<actionControllerConfig>.allow_forgery_protection = <verificationSetting>`
private DataFlow::CallNode getAnAllowForgeryProtectionCall(boolean verificationSetting) {
// exclude some test configuration
not (
result.getLocation().getFile().getRelativePath().matches("%test/%") or
result.getLocation().getFile().getStem() = "test"
) and
result.getReceiver() instanceof ActionControllerConfigNode and
result.asExpr().getExpr().(MethodCall).getMethodName() = "allow_forgery_protection=" and
hasBooleanValue(result.getArgument(0), verificationSetting)
private class LiteralSetting extends Setting {
Literal valueLiteral;
LiteralSetting() {
exists(DataFlow::LocalSourceNode lsn |
lsn.asExpr().getExpr() = valueLiteral and
lsn.flowsTo(this.getArgument(0))
)
}
string getValueText() { result = valueLiteral.getValueText() }
string getSettingString() { result = this.getMethodName() + this.getValueText() }
}
/**
* A node that sets a boolean value.
*/
class BooleanSetting extends LiteralSetting {
override BooleanLiteral valueLiteral;
boolean getValue() { result = valueLiteral.getValue() }
}
}
/**
* A `DataFlow::Node` that may enable or disable Rails CSRF protection in
* production code.
*/
private class AllowForgeryProtectionSetting extends CSRFProtectionSetting::Range {
private boolean verificationSetting;
private class AllowForgeryProtectionSetting extends Settings::BooleanSetting,
CSRFProtectionSetting::Range {
AllowForgeryProtectionSetting() {
this.getReceiver() instanceof Config::ActionControllerNode and
this.getMethodName() = "allow_forgery_protection="
}
AllowForgeryProtectionSetting() { this = getAnAllowForgeryProtectionCall(verificationSetting) }
override boolean getVerificationSetting() { result = verificationSetting }
override boolean getVerificationSetting() { result = this.getValue() }
}
// TODO: initialization hooks, e.g. before_configuration, after_initialize...
// TODO: initializers