@@ -10,6 +10,43 @@ private import codeql.ruby.frameworks.ActiveStorage
1010private import codeql.ruby.frameworks.internal.Rails
1111private import codeql.ruby.ApiGraphs
1212private 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+ }
1350
1451/**
1552 * Provides classes for working with Rails.
@@ -38,37 +75,15 @@ module Rails {
3875 * rendered content.
3976 */
4077 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-
6078 /**
6179 * Gets the template file to be rendered by this call, if any.
6280 */
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 ) }
6782
6883 /**
69- * Get the local variables passed as context to the renderer
84+ * Gets the local variables passed as context to the renderer.
7085 */
71- HashLiteral getLocals ( ) { result = this . getKeywordArgument ( "locals" ) }
86+ HashLiteral getLocals ( ) { result = RenderCallUtils :: getLocals ( this ) }
7287 // TODO: implicit renders in controller actions
7388 }
7489
@@ -287,5 +302,92 @@ private class CookiesSameSiteProtectionSetting extends Settings::NillableStringl
287302 result = "Unsetting 'SameSite' can disable same-site cookie restrictions in some browsers."
288303 }
289304}
305+
290306// TODO: initialization hooks, e.g. before_configuration, after_initialize...
291307// 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