Skip to content

Commit 16639bc

Browse files
committed
Ruby: Model ERB flow for ActionController
Extend the existing modeling for ViewComponent to ActionController. The `render` method in ActionController behaves in a similar way except that the ERB file is evaluated in the context of the controller class, rather than a separate view class. This means we need the value of `self` at the `render` call to flow to the `self` in the ERB file.
1 parent 880b35e commit 16639bc

File tree

6 files changed

+106
-4
lines changed

6 files changed

+106
-4
lines changed

Diff for: ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowDispatch.qll

+51-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import codeql.ruby.dataflow.FlowSummary
88
private import codeql.ruby.dataflow.SSA
99
private import codeql.util.Boolean
1010
private import codeql.util.Unit
11+
private import codeql.ruby.controlflow.CfgNodes
1112

1213
/**
1314
* A `LocalSourceNode` for a `self` variable. This is the implicit `self`
@@ -239,6 +240,7 @@ class NormalCall extends DataFlowCall, TNormalCall {
239240
*/
240241
private module ViewComponentRenderModeling {
241242
private import codeql.ruby.frameworks.ViewComponent
243+
private import codeql.ruby.frameworks.ActionController
242244

243245
private class RenderMethod extends SummarizedCallable, LibraryCallableToIncludeInTypeTracking {
244246
RenderMethod() { this = "render view component" }
@@ -250,12 +252,38 @@ private module ViewComponentRenderModeling {
250252
// use a call-back summary, and adjust it to a method call below
251253
output = "Argument[0].Parameter[self]" and
252254
preservesValue = true
255+
or
256+
input = "Argument[self]" and
257+
output = "Argument[self].Parameter[self]" and
258+
preservesValue = true
253259
}
254260
}
255261

256262
private string invokeToplevelName() { result = "__invoke__toplevel__erb__" }
257263

258-
/** Holds if `call` should be adjusted to be a method call to `name` on `receiver`. */
264+
/**
265+
* Holds if `call` should be adjusted to be a method call to `name` on `receiver`.
266+
* `call` is the callback call inside the flow summary.
267+
* Effectively we generate something like
268+
* ```rb
269+
* def render(view)
270+
* # ViewComponent
271+
* view()
272+
* # ActionController
273+
* self()
274+
* end
275+
* ```
276+
*
277+
* And this adjustment changes it to
278+
* ```rb
279+
* def render(view)
280+
* # ViewComponent
281+
* view.__invoke__toplevel__erb__()
282+
* # ActionController
283+
* self.__invoke__toplevel__erb__()
284+
* end
285+
* ```
286+
*/
259287
predicate adjustedMethodCall(DataFlowCall call, FlowSummaryNode receiver, string name) {
260288
exists(RenderMethod render |
261289
call = TSummaryCall(render, receiver.getSummaryNode()) and
@@ -265,14 +293,29 @@ private module ViewComponentRenderModeling {
265293

266294
/** Holds if `self` belongs to the top-level of an ERB file with matching view class `view`. */
267295
pragma[nomagic]
268-
predicate selfInErbToplevel(SelfVariable self, ViewComponent::ComponentClass view) {
269-
self.getDeclaringScope().(Toplevel).getFile() = view.getTemplate()
296+
predicate selfInErbToplevel(SelfVariable self, Module view) {
297+
self.getDeclaringScope().(Toplevel).getFile() =
298+
[
299+
view.(ViewComponent::ComponentClass).getTemplate(),
300+
view.(ActionControllerClass).getAnAction().getDefaultTemplateFile()
301+
]
270302
}
271303

272304
Toplevel lookupMethod(ViewComponent::ComponentClass m, string name) {
273305
result.getFile() = m.getTemplate() and
274306
name = invokeToplevelName()
275307
}
308+
309+
Toplevel lookupMethodCall(DataFlowCall c, Module mod, DataFlow::Node receiver, string name) {
310+
name = invokeToplevelName() and
311+
exists(ActionControllerActionMethod m, Call summaryCall |
312+
m.getControllerClass() = mod and
313+
result.getFile() = m.getDefaultTemplateFile() and
314+
c.getEnclosingCallable().asLibraryCallable().getACallSimple() = summaryCall and
315+
summaryCall.getEnclosingCallable() = m and
316+
adjustedMethodCall(c, receiver, name)
317+
)
318+
}
276319
}
277320

278321
/** A call for which we want to compute call targets. */
@@ -801,7 +844,11 @@ private CfgScope lookupInstanceMethodCall(DataFlowCall call, string method, bool
801844
exists(Module tp, DataFlow::Node receiver |
802845
methodCall(call, pragma[only_bind_into](receiver), pragma[only_bind_into](method)) and
803846
receiver = trackInstance(tp, exact) and
804-
result = lookupMethod(tp, pragma[only_bind_into](method), exact)
847+
(
848+
result = lookupMethod(tp, pragma[only_bind_into](method), exact)
849+
or
850+
result = ViewComponentRenderModeling::lookupMethodCall(call, tp, receiver, method)
851+
)
805852
)
806853
}
807854

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class UsersController < ActionController::Base
2+
def index
3+
@x = source("index")
4+
render
5+
end
6+
7+
def show
8+
@x = source("show")
9+
# implicit call to `render`
10+
end
11+
12+
def edit
13+
@x = source("edit")
14+
render
15+
end
16+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%=
2+
sink(@x) # $hasValueFlow=edit
3+
%>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%=
2+
sink @x # $hasValueFlow=index
3+
%>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%=
2+
sink @x # $ MISSING: hasValueFlow=show
3+
%>

Diff for: ruby/ql/test/library-tests/dataflow/erb/erb.expected

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
testFailures
2+
| app/views/users/edit.html.erb:2:8:2:9 | @x | Unexpected result: hasValueFlow=index |
3+
| app/views/users/index.html.erb:2:8:2:9 | @x | Unexpected result: hasValueFlow=edit |
24
edges
5+
| app/controllers/users_controller.rb:3:9:3:10 | [post] self [@x] | app/controllers/users_controller.rb:4:9:4:14 | self [@x] | provenance | |
6+
| app/controllers/users_controller.rb:3:14:3:28 | call to source | app/controllers/users_controller.rb:3:9:3:10 | [post] self [@x] | provenance | |
7+
| app/controllers/users_controller.rb:4:9:4:14 | self [@x] | app/views/users/edit.html.erb:2:3:3:1 | self in edit.html.erb [@x] | provenance | |
8+
| app/controllers/users_controller.rb:4:9:4:14 | self [@x] | app/views/users/index.html.erb:2:3:3:1 | self in index.html.erb [@x] | provenance | |
9+
| app/controllers/users_controller.rb:13:9:13:10 | [post] self [@x] | app/controllers/users_controller.rb:14:9:14:14 | self [@x] | provenance | |
10+
| app/controllers/users_controller.rb:13:14:13:27 | call to source | app/controllers/users_controller.rb:13:9:13:10 | [post] self [@x] | provenance | |
11+
| app/controllers/users_controller.rb:14:9:14:14 | self [@x] | app/views/users/edit.html.erb:2:3:3:1 | self in edit.html.erb [@x] | provenance | |
12+
| app/controllers/users_controller.rb:14:9:14:14 | self [@x] | app/views/users/index.html.erb:2:3:3:1 | self in index.html.erb [@x] | provenance | |
13+
| app/views/users/edit.html.erb:2:3:3:1 | self in edit.html.erb [@x] | app/views/users/edit.html.erb:2:8:2:9 | self [@x] | provenance | |
14+
| app/views/users/edit.html.erb:2:8:2:9 | self [@x] | app/views/users/edit.html.erb:2:8:2:9 | @x | provenance | |
15+
| app/views/users/index.html.erb:2:3:3:1 | self in index.html.erb [@x] | app/views/users/index.html.erb:2:8:2:9 | self [@x] | provenance | |
16+
| app/views/users/index.html.erb:2:8:2:9 | self [@x] | app/views/users/index.html.erb:2:8:2:9 | @x | provenance | |
317
| main.rb:3:9:3:9 | x | main.rb:4:26:4:26 | x | provenance | |
418
| main.rb:3:13:3:21 | call to source | main.rb:3:9:3:9 | x | provenance | |
519
| main.rb:4:9:4:12 | view [@x] | main.rb:5:16:5:19 | view [@x] | provenance | |
@@ -45,6 +59,18 @@ edges
4559
| view3.rb:6:5:8:7 | self in get [@x] | view3.rb:7:9:7:10 | self [@x] | provenance | |
4660
| view3.rb:7:9:7:10 | self [@x] | view3.rb:7:9:7:10 | @x | provenance | |
4761
nodes
62+
| app/controllers/users_controller.rb:3:9:3:10 | [post] self [@x] | semmle.label | [post] self [@x] |
63+
| app/controllers/users_controller.rb:3:14:3:28 | call to source | semmle.label | call to source |
64+
| app/controllers/users_controller.rb:4:9:4:14 | self [@x] | semmle.label | self [@x] |
65+
| app/controllers/users_controller.rb:13:9:13:10 | [post] self [@x] | semmle.label | [post] self [@x] |
66+
| app/controllers/users_controller.rb:13:14:13:27 | call to source | semmle.label | call to source |
67+
| app/controllers/users_controller.rb:14:9:14:14 | self [@x] | semmle.label | self [@x] |
68+
| app/views/users/edit.html.erb:2:3:3:1 | self in edit.html.erb [@x] | semmle.label | self in edit.html.erb [@x] |
69+
| app/views/users/edit.html.erb:2:8:2:9 | @x | semmle.label | @x |
70+
| app/views/users/edit.html.erb:2:8:2:9 | self [@x] | semmle.label | self [@x] |
71+
| app/views/users/index.html.erb:2:3:3:1 | self in index.html.erb [@x] | semmle.label | self in index.html.erb [@x] |
72+
| app/views/users/index.html.erb:2:8:2:9 | @x | semmle.label | @x |
73+
| app/views/users/index.html.erb:2:8:2:9 | self [@x] | semmle.label | self [@x] |
4874
| main.rb:3:9:3:9 | x | semmle.label | x |
4975
| main.rb:3:13:3:21 | call to source | semmle.label | call to source |
5076
| main.rb:4:9:4:12 | view [@x] | semmle.label | view [@x] |
@@ -98,6 +124,10 @@ subpaths
98124
| view2.html.erb:3:5:3:13 | call to source | view2.rb:6:13:6:13 | x | view2.rb:7:9:7:10 | [post] self [@x] | view2.html.erb:3:1:3:14 | [post] self [@x] |
99125
| view3.html.erb:3:6:3:8 | self [@x] | view3.rb:6:5:8:7 | self in get [@x] | view3.rb:7:9:7:10 | @x | view3.html.erb:3:6:3:8 | call to get |
100126
#select
127+
| app/views/users/edit.html.erb:2:8:2:9 | @x | app/controllers/users_controller.rb:3:14:3:28 | call to source | app/views/users/edit.html.erb:2:8:2:9 | @x | $@ | app/controllers/users_controller.rb:3:14:3:28 | call to source | call to source |
128+
| app/views/users/edit.html.erb:2:8:2:9 | @x | app/controllers/users_controller.rb:13:14:13:27 | call to source | app/views/users/edit.html.erb:2:8:2:9 | @x | $@ | app/controllers/users_controller.rb:13:14:13:27 | call to source | call to source |
129+
| app/views/users/index.html.erb:2:8:2:9 | @x | app/controllers/users_controller.rb:3:14:3:28 | call to source | app/views/users/index.html.erb:2:8:2:9 | @x | $@ | app/controllers/users_controller.rb:3:14:3:28 | call to source | call to source |
130+
| app/views/users/index.html.erb:2:8:2:9 | @x | app/controllers/users_controller.rb:13:14:13:27 | call to source | app/views/users/index.html.erb:2:8:2:9 | @x | $@ | app/controllers/users_controller.rb:13:14:13:27 | call to source | call to source |
101131
| view1.rb:10:14:10:15 | @x | main.rb:3:13:3:21 | call to source | view1.rb:10:14:10:15 | @x | $@ | main.rb:3:13:3:21 | call to source | call to source |
102132
| view1.rb:10:14:10:15 | @x | view1.html.erb:6:5:6:13 | call to source | view1.rb:10:14:10:15 | @x | $@ | view1.html.erb:6:5:6:13 | call to source | call to source |
103133
| view2.rb:3:14:3:15 | @x | view2.html.erb:3:5:3:13 | call to source | view2.rb:3:14:3:15 | @x | $@ | view2.html.erb:3:5:3:13 | call to source | call to source |

0 commit comments

Comments
 (0)