@@ -12,6 +12,7 @@ private import codeql.ruby.frameworks.ActiveRecord
1212private import codeql.ruby.frameworks.ActiveStorage
1313private import codeql.ruby.ast.internal.Module
1414private import codeql.ruby.ApiGraphs
15+ private import codeql.ruby.security.OpenSSL
1516
1617/**
1718 * A reference to either `Rails::Railtie`, `Rails::Engine`, or `Rails::Application`.
@@ -47,85 +48,220 @@ private DataFlow::CallNode getAConfigureCallNode() {
4748}
4849
4950/**
50- * An access to a Rails config object.
51+ * Classes representing accesses to the Rails config object.
5152 */
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 ) ) }
7790 }
78- }
7991
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+ }
82119}
83120
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 ( ) }
87180
88- MethodCall getCall ( ) { result = this .asExpr ( ) .getExpr ( ) }
181+ predicate isNilValue ( ) { valueLiteral instanceof NilLiteral }
182+ }
89183}
90184
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="
96194 }
195+
196+ override boolean getVerificationSetting ( ) { result = this .getValue ( ) }
97197}
98198
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+ }
105219}
106220
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+ }
117238}
118239
240+ // TODO: this may also take a proc that specifies how to handle specific requests
119241/**
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
122247 */
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+ }
125254
126- AllowForgeryProtectionSetting ( ) { this = getAnAllowForgeryProtectionCall ( verificationSetting ) }
255+ string getDefaultValue ( ) { result = "lax" }
127256
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+ }
129265}
130266// TODO: initialization hooks, e.g. before_configuration, after_initialize...
131267// TODO: initializers
0 commit comments