@@ -10,6 +10,43 @@ private import codeql.ruby.frameworks.ActiveStorage
10
10
private import codeql.ruby.frameworks.internal.Rails
11
11
private import codeql.ruby.ApiGraphs
12
12
private import codeql.ruby.security.OpenSSL
13
+ private import codeql.ruby.dataflow.FlowSummary
14
+
15
+ /** Provides utility predicates for extracting information from calls to `render`. */
16
+ private module RenderCallUtils {
17
+ private Expr getTemplatePathArgument ( MethodCall renderCall ) {
18
+ // TODO: support other ways of specifying paths (e.g. `file`)
19
+ result =
20
+ [ renderCall .getKeywordArgument ( [ "partial" , "template" , "action" ] ) , renderCall .getArgument ( 0 ) ]
21
+ }
22
+
23
+ private string getTemplatePathValue ( MethodCall renderCall ) {
24
+ result = getTemplatePathArgument ( renderCall ) .getConstantValue ( ) .getStringlikeValue ( )
25
+ }
26
+
27
+ // everything up to and including the final slash, but ignoring any leading slash
28
+ private string getSubPath ( MethodCall renderCall ) {
29
+ result = getTemplatePathValue ( renderCall ) .regexpCapture ( "^/?(.*/)?(?:[^/]*?)$" , 1 )
30
+ }
31
+
32
+ // everything after the final slash, or the whole string if there is no slash
33
+ private string getBaseName ( MethodCall renderCall ) {
34
+ result = getTemplatePathValue ( renderCall ) .regexpCapture ( "^/?(?:.*/)?([^/]*?)$" , 1 )
35
+ }
36
+
37
+ /**
38
+ * Gets the template file to be rendered by this render call, if any.
39
+ */
40
+ ErbFile getTemplateFile ( MethodCall renderCall ) {
41
+ result .getTemplateName ( ) = getBaseName ( renderCall ) and
42
+ result .getRelativePath ( ) .matches ( "%app/views/" + getSubPath ( renderCall ) + "%" )
43
+ }
44
+
45
+ /**
46
+ * Gets the local variables passed as context to the renderer.
47
+ */
48
+ HashLiteral getLocals ( MethodCall renderCall ) { result = renderCall .getKeywordArgument ( "locals" ) }
49
+ }
13
50
14
51
/**
15
52
* Provides classes for working with Rails.
@@ -38,37 +75,15 @@ module Rails {
38
75
* rendered content.
39
76
*/
40
77
class RenderCall extends MethodCall instanceof RenderCallImpl {
41
- private Expr getTemplatePathArgument ( ) {
42
- // TODO: support other ways of specifying paths (e.g. `file`)
43
- result = [ this .getKeywordArgument ( [ "partial" , "template" , "action" ] ) , this .getArgument ( 0 ) ]
44
- }
45
-
46
- private string getTemplatePathValue ( ) {
47
- result = this .getTemplatePathArgument ( ) .getConstantValue ( ) .getStringlikeValue ( )
48
- }
49
-
50
- // everything up to and including the final slash, but ignoring any leading slash
51
- private string getSubPath ( ) {
52
- result = this .getTemplatePathValue ( ) .regexpCapture ( "^/?(.*/)?(?:[^/]*?)$" , 1 )
53
- }
54
-
55
- // everything after the final slash, or the whole string if there is no slash
56
- private string getBaseName ( ) {
57
- result = this .getTemplatePathValue ( ) .regexpCapture ( "^/?(?:.*/)?([^/]*?)$" , 1 )
58
- }
59
-
60
78
/**
61
79
* Gets the template file to be rendered by this call, if any.
62
80
*/
63
- ErbFile getTemplateFile ( ) {
64
- result .getTemplateName ( ) = this .getBaseName ( ) and
65
- result .getRelativePath ( ) .matches ( "%app/views/" + this .getSubPath ( ) + "%" )
66
- }
81
+ ErbFile getTemplateFile ( ) { result = RenderCallUtils:: getTemplateFile ( this ) }
67
82
68
83
/**
69
- * Get the local variables passed as context to the renderer
84
+ * Gets the local variables passed as context to the renderer.
70
85
*/
71
- HashLiteral getLocals ( ) { result = this . getKeywordArgument ( "locals" ) }
86
+ HashLiteral getLocals ( ) { result = RenderCallUtils :: getLocals ( this ) }
72
87
// TODO: implicit renders in controller actions
73
88
}
74
89
@@ -287,5 +302,92 @@ private class CookiesSameSiteProtectionSetting extends Settings::NillableStringl
287
302
result = "Unsetting 'SameSite' can disable same-site cookie restrictions in some browsers."
288
303
}
289
304
}
305
+
290
306
// TODO: initialization hooks, e.g. before_configuration, after_initialize...
291
307
// TODO: initializers
308
+ /** A synthetic global to represent the value passed to the `locals` argument of a render call for a specific ERB file. */
309
+ private class LocalAssignsHashSyntheticGlobal extends SummaryComponent:: SyntheticGlobal {
310
+ private ErbFile erbFile ;
311
+ private string id ;
312
+ // Note that we can't use an actual `Rails::RenderCall` here due to problems with non-monotonic recursion
313
+ private MethodCall renderCall ;
314
+
315
+ LocalAssignsHashSyntheticGlobal ( ) {
316
+ this = "LocalAssignsHashSyntheticGlobal+" + id and
317
+ id = erbFile .getRelativePath ( ) + "+" + renderCall .getLocation ( ) and
318
+ renderCall .getMethodName ( ) = "render" and
319
+ RenderCallUtils:: getTemplateFile ( renderCall ) = erbFile
320
+ }
321
+
322
+ /** Gets the `ErbFile` which this locals hash is accessible from. */
323
+ ErbFile getErbFile ( ) { result = erbFile }
324
+
325
+ /** Gets the identifier for this particular locals hash synthetic global. */
326
+ string getId ( ) { result = id }
327
+
328
+ /** Gets a call to render that can write to this hash. */
329
+ Rails:: RenderCall getARenderCall ( ) { result = renderCall }
330
+ }
331
+
332
+ /** A summary for `render` calls linked to some specific ERB file. */
333
+ private class RenderLocalsSummary extends SummarizedCallable {
334
+ private LocalAssignsHashSyntheticGlobal glob ;
335
+
336
+ RenderLocalsSummary ( ) { this = "rails_render_locals()" + glob .getId ( ) }
337
+
338
+ override Rails:: RenderCall getACall ( ) { result = glob .getARenderCall ( ) }
339
+
340
+ override predicate propagatesFlowExt ( string input , string output , boolean preservesValue ) {
341
+ input = "Argument[locals:]" and
342
+ output = "SyntheticGlobal[" + glob + "]" and
343
+ preservesValue = true
344
+ }
345
+ }
346
+
347
+ /** A summary for calls to `local_assigns` in a view to access a `render` call `locals` hash. */
348
+ private class AccessLocalsSummary extends SummarizedCallable {
349
+ private LocalAssignsHashSyntheticGlobal glob ;
350
+
351
+ AccessLocalsSummary ( ) { this = "rails_local_assigns()" + glob .getId ( ) }
352
+
353
+ override MethodCall getACall ( ) {
354
+ glob .getErbFile ( ) = result .getLocation ( ) .getFile ( ) and
355
+ result .getMethodName ( ) = "local_assigns"
356
+ }
357
+
358
+ override predicate propagatesFlowExt ( string input , string output , boolean preservesValue ) {
359
+ input = "SyntheticGlobal[" + glob + "]" and
360
+ output = "ReturnValue" and
361
+ preservesValue = true
362
+ }
363
+ }
364
+
365
+ private string getAMethodNameFromErbFile ( ErbFile f ) {
366
+ result = any ( MethodCall c | c .getLocation ( ) .getFile ( ) = f ) .getMethodName ( )
367
+ }
368
+
369
+ private class AccessLocalsKeySummary extends SummarizedCallable {
370
+ private LocalAssignsHashSyntheticGlobal glob ;
371
+ private string methodName ;
372
+
373
+ AccessLocalsKeySummary ( ) {
374
+ this = "rails_locals_key()" + glob .getId ( ) + "#" + methodName and
375
+ methodName = getAMethodNameFromErbFile ( glob .getErbFile ( ) ) and
376
+ // Limit method calls to those that could plausibly be a key in a `locals` hash argument
377
+ // TODO: this could be more precise but for problems using the dataflow library in this context
378
+ methodName =
379
+ any ( HashLiteral l ) .getAKeyValuePair ( ) .getKey ( ) .getConstantValue ( ) .getStringlikeValue ( )
380
+ }
381
+
382
+ override MethodCall getACall ( ) {
383
+ result .getLocation ( ) .getFile ( ) = glob .getErbFile ( ) and
384
+ result .getMethodName ( ) = methodName and
385
+ result .getReceiver ( ) instanceof SelfVariableReadAccess
386
+ }
387
+
388
+ override predicate propagatesFlowExt ( string input , string output , boolean preservesValue ) {
389
+ input = "SyntheticGlobal[" + glob + "].Element[:" + methodName + "]" and
390
+ output = "ReturnValue" and
391
+ preservesValue = true
392
+ }
393
+ }
0 commit comments