Skip to content

Commit 4666024

Browse files
committed
model some ways to configure Rails
1 parent 91f99ed commit 4666024

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ private import codeql.ruby.frameworks.ActionController
66
private import codeql.ruby.frameworks.ActiveRecord
77
private import codeql.ruby.frameworks.ActiveStorage
88
private import codeql.ruby.frameworks.ActionView
9+
private import codeql.ruby.frameworks.Rails
910
private import codeql.ruby.frameworks.StandardLibrary
1011
private import codeql.ruby.frameworks.Files
1112
private import codeql.ruby.frameworks.HttpClients
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* Provides classes for working with Rails.
3+
*/
4+
5+
private import codeql.files.FileSystem
6+
private import codeql.ruby.AST
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.DataFlow
9+
private import codeql.ruby.frameworks.ActionController
10+
private import codeql.ruby.frameworks.ActionView
11+
private import codeql.ruby.frameworks.ActiveRecord
12+
private import codeql.ruby.frameworks.ActiveStorage
13+
private import codeql.ruby.ast.internal.Module
14+
private import codeql.ruby.ApiGraphs
15+
16+
/**
17+
* A reference to either `Rails::Railtie`, `Rails::Engine`, or `Rails::Application`.
18+
* `Engine` and `Application` extend `Railtie`, but may not have definitions present in the database.
19+
*/
20+
private class RailtieClassAccess extends ConstantReadAccess {
21+
RailtieClassAccess() {
22+
this.getScopeExpr().(ConstantAccess).getName() = "Rails" and
23+
this.getName() = ["Railtie", "Engine", "Application"]
24+
}
25+
}
26+
27+
// A `ClassDeclaration` that (transitively) extends `Rails::Railtie`
28+
private class RailtieClass extends ClassDeclaration {
29+
RailtieClass() {
30+
this.getSuperclassExpr() instanceof RailtieClassAccess or
31+
exists(RailtieClass other | other.getModule() = resolveScopeExpr(this.getSuperclassExpr()))
32+
}
33+
}
34+
35+
// `Rails.application.config` calls
36+
private API::Node getConfigApiNode() {
37+
result = API::getTopLevelMember("Rails").getReturn("application").getReturn("config")
38+
}
39+
40+
// `Rails.application.configure`
41+
private DataFlow::CallNode getConfigureApiNode() {
42+
result = API::getTopLevelMember("Rails").getReturn("application").getAMethodCall("configure")
43+
}
44+
45+
private DataFlow::CallNode getAConfigureCallNode() {
46+
// `Rails.application.configure`
47+
result = getConfigureApiNode()
48+
or
49+
// `Rails::Application.configure`
50+
exists(ConstantReadAccess read, RailtieClass cls |
51+
read = result.getReceiver().asExpr().getExpr() and
52+
resolveScopeExpr(read) = cls.getModule() and
53+
result.asExpr().getExpr().(MethodCall).getMethodName() = "configure"
54+
)
55+
}
56+
57+
/**
58+
* An access to a Rails config object.
59+
*/
60+
private class ConfigSourceNode extends DataFlow::LocalSourceNode {
61+
ConfigSourceNode() {
62+
// `Foo < Rails::Application ... config ...`
63+
exists(MethodCall configCall | this.asExpr().getExpr() = configCall |
64+
configCall.getMethodName() = "config" and
65+
configCall.getEnclosingModule() instanceof RailtieClass
66+
)
67+
or
68+
// `Rails.application.config`
69+
this = getConfigApiNode().getAnImmediateUse()
70+
or
71+
// `Rails.application.configure { ... config ... }`
72+
// `Rails::Application.configure { ... config ... }`
73+
exists(DataFlow::CallNode configureCallNode, Block block, MethodCall configCall |
74+
configCall = this.asExpr().getExpr()
75+
|
76+
configureCallNode = getAConfigureCallNode() and
77+
block = configureCallNode.asExpr().getExpr().(MethodCall).getBlock() and
78+
configCall.getParent+() = block and
79+
configCall.getMethodName() = "config"
80+
)
81+
}
82+
}
83+
84+
private class ConfigNode extends DataFlow::Node {
85+
ConfigNode() { exists(ConfigSourceNode src | src.flowsTo(this)) }
86+
}
87+
88+
// A call where the Rails application config is the receiver
89+
private class CallAgainstConfig extends DataFlow::CallNode {
90+
CallAgainstConfig() { this.getReceiver() instanceof ConfigNode }
91+
92+
MethodCall getCall() { result = this.asExpr().getExpr() }
93+
94+
Block getBlock() { result = this.getCall().getBlock() }
95+
}
96+
97+
private class ActionControllerConfigNode extends DataFlow::Node {
98+
ActionControllerConfigNode() {
99+
exists(CallAgainstConfig source | source.getCall().getMethodName() = "action_controller" |
100+
source.flowsTo(this)
101+
)
102+
}
103+
}
104+
105+
/** Holds if `node` can contain `value`. */
106+
private predicate hasBooleanValue(DataFlow::Node node, boolean value) {
107+
exists(DataFlow::LocalSourceNode literal |
108+
literal.asExpr().getExpr().(BooleanLiteral).getValue() = value and
109+
literal.flowsTo(node)
110+
)
111+
}
112+
113+
// `<actionControllerConfig>.allow_forgery_protection = <verificationSetting>`
114+
private DataFlow::CallNode getAnAllowForgeryProtectionCall(boolean verificationSetting) {
115+
exists(ActionControllerConfigNode recv |
116+
// exclude some test and development configuration
117+
not (
118+
result.getLocation().getFile().getRelativePath().matches("%test/%") or
119+
result.getLocation().getFile().getStem() = ["test", "development"]
120+
) and
121+
result.getReceiver() = recv and
122+
result.asExpr().getExpr().(MethodCall).getMethodName() = "allow_forgery_protection=" and
123+
hasBooleanValue(result.getArgument(0), verificationSetting)
124+
)
125+
}
126+
127+
/**
128+
* A `DataFlow::Node` that may enable or disable Rails CSRF protection in
129+
* production code.
130+
*/
131+
private class AllowForgeryProtectionSetting extends CSRFProtectionSetting::Range {
132+
private boolean verificationSetting;
133+
134+
AllowForgeryProtectionSetting() { this = getAnAllowForgeryProtectionCall(verificationSetting) }
135+
136+
override boolean getVerificationSetting() { result = verificationSetting }
137+
}
138+
// TODO: initialization hooks, e.g. before_configuration, after_initialize...
139+
// TODO: initializers

0 commit comments

Comments
 (0)