@@ -12,6 +12,7 @@ private import codeql.ruby.frameworks.ActiveRecord
12
12
private import codeql.ruby.frameworks.ActiveStorage
13
13
private import codeql.ruby.ast.internal.Module
14
14
private import codeql.ruby.ApiGraphs
15
+ private import codeql.ruby.security.OpenSSL
15
16
16
17
/**
17
18
* A reference to either `Rails::Railtie`, `Rails::Engine`, or `Rails::Application`.
@@ -47,85 +48,220 @@ private DataFlow::CallNode getAConfigureCallNode() {
47
48
}
48
49
49
50
/**
50
- * An access to a Rails config object.
51
+ * Classes representing accesses to the Rails config object.
51
52
*/
52
- private class ConfigSourceNode extends DataFlow:: LocalSourceNode {
53
- ConfigSourceNode ( ) {
54
- // `Foo < Rails::Application ... config ...`
55
- exists ( MethodCall configCall | this .asExpr ( ) .getExpr ( ) = configCall |
56
- configCall .getMethodName ( ) = "config" and
57
- configCall .getEnclosingModule ( ) instanceof RailtieClass
58
- )
59
- or
60
- // `Rails.application.config`
61
- this =
62
- API:: getTopLevelMember ( "Rails" )
63
- .getReturn ( "application" )
64
- .getReturn ( "config" )
65
- .getAnImmediateUse ( )
66
- or
67
- // `Rails.application.configure { ... config ... }`
68
- // `Rails::Application.configure { ... config ... }`
69
- exists ( DataFlow:: CallNode configureCallNode , Block block , MethodCall configCall |
70
- configCall = this .asExpr ( ) .getExpr ( )
71
- |
72
- configureCallNode = getAConfigureCallNode ( ) and
73
- block = configureCallNode .getBlock ( ) .asExpr ( ) .getExpr ( ) and
74
- configCall .getParent + ( ) = block and
75
- configCall .getMethodName ( ) = "config"
76
- )
53
+ private module Config {
54
+ /**
55
+ * An access to a Rails config object.
56
+ */
57
+ private class SourceNode extends DataFlow:: LocalSourceNode {
58
+ SourceNode ( ) {
59
+ // `Foo < Rails::Application ... config ...`
60
+ exists ( MethodCall configCall | this .asExpr ( ) .getExpr ( ) = configCall |
61
+ configCall .getMethodName ( ) = "config" and
62
+ configCall .getEnclosingModule ( ) instanceof RailtieClass
63
+ )
64
+ or
65
+ // `Rails.application.config`
66
+ this =
67
+ API:: getTopLevelMember ( "Rails" )
68
+ .getReturn ( "application" )
69
+ .getReturn ( "config" )
70
+ .getAnImmediateUse ( )
71
+ or
72
+ // `Rails.application.configure { ... config ... }`
73
+ // `Rails::Application.configure { ... config ... }`
74
+ exists ( DataFlow:: CallNode configureCallNode , Block block , MethodCall configCall |
75
+ configCall = this .asExpr ( ) .getExpr ( )
76
+ |
77
+ configureCallNode = getAConfigureCallNode ( ) and
78
+ block = configureCallNode .asExpr ( ) .getExpr ( ) .( MethodCall ) .getBlock ( ) and
79
+ configCall .getParent + ( ) = block and
80
+ configCall .getMethodName ( ) = "config"
81
+ )
82
+ }
83
+ }
84
+
85
+ /**
86
+ * A reference to the Rails config object.
87
+ */
88
+ class Node extends DataFlow:: Node {
89
+ Node ( ) { exists ( SourceNode src | src .flowsTo ( this ) ) }
77
90
}
78
- }
79
91
80
- private class ConfigNode extends DataFlow:: Node {
81
- ConfigNode ( ) { exists ( ConfigSourceNode src | src .flowsTo ( this ) ) }
92
+ /**
93
+ * A reference to the ActionController config object.
94
+ */
95
+ class ActionControllerNode extends DataFlow:: Node {
96
+ ActionControllerNode ( ) {
97
+ exists ( DataFlow:: CallNode source |
98
+ source .getReceiver ( ) instanceof Node and
99
+ source .getMethodName ( ) = "action_controller"
100
+ |
101
+ source .flowsTo ( this )
102
+ )
103
+ }
104
+ }
105
+
106
+ /**
107
+ * A reference to the ActionDispatch config object.
108
+ */
109
+ class ActionDispatchNode extends DataFlow:: Node {
110
+ ActionDispatchNode ( ) {
111
+ exists ( DataFlow:: CallNode source |
112
+ source .getReceiver ( ) instanceof Node and
113
+ source .getMethodName ( ) = "action_dispatch"
114
+ |
115
+ source .flowsTo ( this )
116
+ )
117
+ }
118
+ }
82
119
}
83
120
84
- // A call where the Rails application config is the receiver
85
- private class CallAgainstConfig extends DataFlow:: CallNode {
86
- CallAgainstConfig ( ) { this .getReceiver ( ) instanceof ConfigNode }
121
+ /**
122
+ * Classes representing nodes that set a Rails configuration value.
123
+ */
124
+ private module Settings {
125
+ private predicate isInTestConfiguration ( Location loc ) {
126
+ loc .getFile ( ) .getRelativePath ( ) .matches ( "%test/%" ) or
127
+ loc .getFile ( ) .getStem ( ) = "test"
128
+ }
129
+
130
+ private class Setting extends DataFlow:: CallNode {
131
+ Setting ( ) {
132
+ // exclude some test configuration
133
+ not isInTestConfiguration ( this .getLocation ( ) ) and
134
+ this .getReceiver + ( ) instanceof Config:: Node and
135
+ this .asExpr ( ) .getExpr ( ) instanceof SetterMethodCall
136
+ }
137
+ }
138
+
139
+ private class LiteralSetting extends Setting {
140
+ Literal valueLiteral ;
141
+
142
+ LiteralSetting ( ) {
143
+ exists ( DataFlow:: LocalSourceNode lsn |
144
+ lsn .asExpr ( ) .getExpr ( ) = valueLiteral and
145
+ lsn .flowsTo ( this .getArgument ( 0 ) )
146
+ )
147
+ }
148
+
149
+ string getValueText ( ) { result = valueLiteral .getValueText ( ) }
150
+
151
+ string getSettingString ( ) { result = this .getMethodName ( ) + this .getValueText ( ) }
152
+ }
153
+
154
+ /**
155
+ * A node that sets a boolean value.
156
+ */
157
+ class BooleanSetting extends LiteralSetting {
158
+ override BooleanLiteral valueLiteral ;
159
+
160
+ boolean getValue ( ) { result = valueLiteral .getValue ( ) }
161
+ }
162
+
163
+ /**
164
+ * A node that sets a Stringlike value.
165
+ */
166
+ class StringlikeSetting extends LiteralSetting {
167
+ override StringlikeLiteral valueLiteral ;
168
+ }
169
+
170
+ /**
171
+ * A node that sets a Stringlike value, or `nil`.
172
+ */
173
+ class NillableStringlikeSetting extends LiteralSetting {
174
+ NillableStringlikeSetting ( ) {
175
+ valueLiteral instanceof StringlikeLiteral or
176
+ valueLiteral instanceof NilLiteral
177
+ }
178
+
179
+ string getStringValue ( ) { result = valueLiteral .( StringlikeLiteral ) .getValueText ( ) }
87
180
88
- MethodCall getCall ( ) { result = this .asExpr ( ) .getExpr ( ) }
181
+ predicate isNilValue ( ) { valueLiteral instanceof NilLiteral }
182
+ }
89
183
}
90
184
91
- private class ActionControllerConfigNode extends DataFlow:: Node {
92
- ActionControllerConfigNode ( ) {
93
- exists ( CallAgainstConfig source | source .getCall ( ) .getMethodName ( ) = "action_controller" |
94
- source .flowsTo ( this )
95
- )
185
+ /**
186
+ * A `DataFlow::Node` that may enable or disable Rails CSRF protection in
187
+ * production code.
188
+ */
189
+ private class AllowForgeryProtectionSetting extends Settings:: BooleanSetting ,
190
+ CSRFProtectionSetting:: Range {
191
+ AllowForgeryProtectionSetting ( ) {
192
+ this .getReceiver ( ) instanceof Config:: ActionControllerNode and
193
+ this .getMethodName ( ) = "allow_forgery_protection="
96
194
}
195
+
196
+ override boolean getVerificationSetting ( ) { result = this .getValue ( ) }
97
197
}
98
198
99
- /** Holds if `node` can contain `value`. */
100
- private predicate hasBooleanValue ( DataFlow:: Node node , boolean value ) {
101
- exists ( DataFlow:: LocalSourceNode literal |
102
- literal .asExpr ( ) .getExpr ( ) .( BooleanLiteral ) .getValue ( ) = value and
103
- literal .flowsTo ( node )
104
- )
199
+ /**
200
+ * Sets the cipher to be used for encrypted cookies. Defaults to "aes-256-gcm".
201
+ * This can be set to any cipher supported by
202
+ * https://ruby-doc.org/stdlib-2.7.1/libdoc/openssl/rdoc/OpenSSL/Cipher.html
203
+ */
204
+ private class EncryptedCookieCipherSetting extends Settings:: StringlikeSetting ,
205
+ CookieSecurityConfigurationSetting:: Range {
206
+ EncryptedCookieCipherSetting ( ) {
207
+ this .getReceiver ( ) instanceof Config:: ActionDispatchNode and
208
+ this .getMethodName ( ) = "encrypted_cookie_cipher="
209
+ }
210
+
211
+ OpenSSLCipher getCipher ( ) { this .getValueText ( ) = result .getName ( ) }
212
+
213
+ OpenSSLCipher getDefaultCipher ( ) { result .getName ( ) = "aes-256-gcm" }
214
+
215
+ override string getSecurityWarningMessage ( ) {
216
+ this .getCipher ( ) .isWeak ( ) and
217
+ result = this .getValueText ( ) + " is a weak cipher."
218
+ }
105
219
}
106
220
107
- // `<actionControllerConfig>.allow_forgery_protection = <verificationSetting>`
108
- private DataFlow:: CallNode getAnAllowForgeryProtectionCall ( boolean verificationSetting ) {
109
- // exclude some test configuration
110
- not (
111
- result .getLocation ( ) .getFile ( ) .getRelativePath ( ) .matches ( "%test/%" ) or
112
- result .getLocation ( ) .getFile ( ) .getStem ( ) = "test"
113
- ) and
114
- result .getReceiver ( ) instanceof ActionControllerConfigNode and
115
- result .asExpr ( ) .getExpr ( ) .( MethodCall ) .getMethodName ( ) = "allow_forgery_protection=" and
116
- hasBooleanValue ( result .getArgument ( 0 ) , verificationSetting )
221
+ /**
222
+ * If true, signed and encrypted cookies will use the AES-256-GCM cipher rather
223
+ * than the older AES-256-CBC cipher. Defaults to true.
224
+ */
225
+ private class UseAuthenticatedCookieEncryptionSetting extends Settings:: BooleanSetting ,
226
+ CookieSecurityConfigurationSetting:: Range {
227
+ UseAuthenticatedCookieEncryptionSetting ( ) {
228
+ this .getReceiver ( ) instanceof Config:: ActionDispatchNode and
229
+ this .getMethodName ( ) = "use_authenticated_cookie_encryption="
230
+ }
231
+
232
+ boolean getDefaultValue ( ) { result = true }
233
+
234
+ override string getSecurityWarningMessage ( ) {
235
+ this .getValue ( ) = false and
236
+ result = this .getSettingString ( ) + " selects a weaker block mode for authenticated cookies."
237
+ }
117
238
}
118
239
240
+ // TODO: this may also take a proc that specifies how to handle specific requests
119
241
/**
120
- * A `DataFlow::Node` that may enable or disable Rails CSRF protection in
121
- * production code.
242
+ * Configures the default value of the `SameSite` attribute when setting cookies.
243
+ * Valid string values are `strict`, `lax`, and `none`.
244
+ * The attribute can be omitted by setting this to `nil`.
245
+ * The default if unset is `:lax`.
246
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#strict
122
247
*/
123
- private class AllowForgeryProtectionSetting extends CSRFProtectionSetting:: Range {
124
- private boolean verificationSetting ;
248
+ private class CookiesSameSiteProtectionSetting extends Settings:: NillableStringlikeSetting ,
249
+ CookieSecurityConfigurationSetting:: Range {
250
+ CookiesSameSiteProtectionSetting ( ) {
251
+ this .getReceiver ( ) instanceof Config:: ActionDispatchNode and
252
+ this .getMethodName ( ) = "cookies_same_site_protection="
253
+ }
125
254
126
- AllowForgeryProtectionSetting ( ) { this = getAnAllowForgeryProtectionCall ( verificationSetting ) }
255
+ string getDefaultValue ( ) { result = "lax" }
127
256
128
- override boolean getVerificationSetting ( ) { result = verificationSetting }
257
+ override string getSecurityWarningMessage ( ) {
258
+ // Mark unset as being potentially dangerous, as not all browsers default to "lax"
259
+ this .getStringValue ( ) .toLowerCase ( ) = "none" and
260
+ result = "Setting 'SameSite' to 'None' may make an application more vulnerable to CSRF attacks."
261
+ or
262
+ this .isNilValue ( ) and
263
+ result = "Unsetting 'SameSite' can disable same-site cookie restrictions in some browsers."
264
+ }
129
265
}
130
266
// TODO: initialization hooks, e.g. before_configuration, after_initialize...
131
267
// TODO: initializers
0 commit comments