Skip to content

Commit fb86ef4

Browse files
committed
Ruby: Model ActionController filters
ActionController filters provide a way to register callbacks that run before, after or around an action (i.e. HTTP request handler). They run in the same class context as the action, so can get/set instance variables and generally interact with the action in arbitrary ways. In order to track flow between filters and actions, we have to model the callback chain. This commit does that. A later change will add dataflow steps to actually track flow through the chain.
1 parent 6c0b50c commit fb86ef4

File tree

12 files changed

+636
-54
lines changed

12 files changed

+636
-54
lines changed

ruby/ql/lib/codeql/ruby/frameworks/ActionController.qll

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ private import codeql.ruby.frameworks.Rails
1414
private import codeql.ruby.frameworks.internal.Rails
1515
private import codeql.ruby.dataflow.internal.DataFlowDispatch
1616

17+
/**
18+
* Provides modeling for ActionController, which is part of the `actionpack` gem.
19+
* Version: 7.0.
20+
*/
21+
module ActionController {
22+
// TODO: move the rest of this file inside this module.
23+
import codeql.ruby.frameworks.actioncontroller.Filters
24+
}
25+
1726
/**
1827
* DEPRECATED: Import `codeql.ruby.frameworks.Rails` and use `Rails::ParamsCall` instead.
1928
*/
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
/**
2+
* Provides modeling for ActionController filters.
3+
*/
4+
5+
private import codeql.ruby.AST
6+
private import codeql.ruby.frameworks.ActionController
7+
private import codeql.ruby.controlflow.CfgNodes
8+
private import codeql.ruby.controlflow.CfgNodes::ExprNodes
9+
private import codeql.ruby.DataFlow
10+
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
11+
private import codeql.ruby.ast.internal.Constant
12+
13+
/**
14+
* Provides modeling for ActionController filters.
15+
*/
16+
module Filters {
17+
private newtype TFilterKind =
18+
TBeforeKind() or
19+
TAfterKind() or
20+
TAroundKind()
21+
22+
/**
23+
* Represents the kind of filter.
24+
* "before" filters run before the action and "after" filters run after the
25+
* action. "around" filters run around the action, `yield`ing to it at will.
26+
*/
27+
private class FilterKind extends TFilterKind {
28+
string toString() {
29+
this = TBeforeKind() and result = "before"
30+
or
31+
this = TAfterKind() and result = "after"
32+
or
33+
this = TAroundKind() and result = "around"
34+
}
35+
}
36+
37+
/**
38+
* A call to a class method that adds or removes a filter from the callback chain.
39+
* This class exists to encapsulate common behavior between calls that
40+
* register callbacks (`before_action`, `after_action` etc.) and calls that
41+
* remove callbacks (`skip_before_action`, `skip_after_action` etc.)
42+
*/
43+
private class FilterCall extends MethodCallCfgNode {
44+
private FilterKind kind;
45+
46+
FilterCall() {
47+
this.getExpr().getEnclosingModule() = any(ActionControllerClass c).getADeclaration() and
48+
this.getMethodName() = ["", "prepend_", "append_", "skip_"] + kind + "_action"
49+
}
50+
51+
FilterKind getKind() { result = kind }
52+
53+
/**
54+
* Gets an action which this filter is applied to.
55+
*/
56+
ActionControllerActionMethod getAction() {
57+
// A filter cannot apply to another filter
58+
result != any(Filter f).getFilterCallable() and
59+
// Only include routable actions. This can exclude valid actions if we can't parse the `routes.rb` file fully.
60+
exists(result.getARoute()) and
61+
(
62+
result.getName() = this.getOnlyArgument()
63+
or
64+
not exists(this.getOnlyArgument()) and
65+
forall(string except | except = this.getExceptArgument() | result.getName() != except)
66+
) and
67+
(
68+
result = this.getExpr().getEnclosingModule().getAMethod()
69+
or
70+
exists(ModuleBase m |
71+
m.getModule() = this.getExpr().getEnclosingModule().getModule().getADescendent() and
72+
result = m.getAMethod()
73+
)
74+
)
75+
}
76+
77+
private string getOnlyArgument() {
78+
exists(ExprCfgNode only | only = this.getKeywordArgument("only") |
79+
// only: :index
80+
result = only.getConstantValue().getStringlikeValue()
81+
or
82+
// only: [:index, :show]
83+
// only: SOME_CONST_ARRAY
84+
exists(ArrayLiteralCfgNode n |
85+
isArrayConstant(only, n) and
86+
result = n.getAnArgument().getConstantValue().getStringlikeValue()
87+
)
88+
)
89+
}
90+
91+
private string getExceptArgument() {
92+
exists(ExprCfgNode except | except = this.getKeywordArgument("except") |
93+
// except: :create
94+
result = except.getConstantValue().getStringlikeValue()
95+
or
96+
// except: [:create, :update]
97+
result =
98+
except.(ArrayLiteralCfgNode).getAnArgument().getConstantValue().getStringlikeValue()
99+
)
100+
}
101+
102+
StringlikeLiteralCfgNode getFilterArgument() { result = this.getPositionalArgument(_) }
103+
104+
/**
105+
* Gets the callable that implements the filter with name `name`.
106+
* This currently only finds methods in the local class or superclass.
107+
* It doesn't handle:
108+
* - lambdas
109+
* - blocks
110+
* - classes
111+
*
112+
* In the example below, the callable for the filter `:foo` is the method `foo`.
113+
* ```rb
114+
* class PostsController < ActionController::Base
115+
* before_action :foo
116+
*
117+
* def foo
118+
* end
119+
* end
120+
* ```
121+
*/
122+
Callable getFilterCallable(string name) {
123+
result.(MethodBase).getName() = name and
124+
// Method in same class
125+
(
126+
result.getEnclosingModule() = this.getExpr().getEnclosingModule()
127+
or
128+
// Method in superclass
129+
result.getEnclosingModule().getModule() =
130+
this.getExpr().getEnclosingModule().getModule().getSuperClass()
131+
)
132+
}
133+
}
134+
135+
/**
136+
* An argument to a call that registers a callback for one or more
137+
* ActionController actions. These are commonly called filters.
138+
*
139+
* In the example below, the `before_action` call registers `set_user` as a
140+
* callback for all actions in the controller. When a request is routed to
141+
* `PostsController#index`, the method `set_user` will be called before
142+
* `index` is executed.
143+
*
144+
* The `after_action` call registers `log_request` as a callback. This behaves
145+
* similarly to `before_action` but the callback will be called _after_ the
146+
* action has finished executing.
147+
*
148+
* The `around_action` call registers `trace_request` as a callback that will
149+
* run _around_ the action. This means that `trace_request` will be called
150+
* before the action, and will run until it `yield`s control. Then the action
151+
* (or another callback) will be run. Once the action has run, control will be
152+
* returned to `trace_request`, which will finish executing.
153+
*
154+
* Due to the complexity of dataflow around `yield` calls, currently only
155+
* `before_action` and `after_action` callbacks are modeled fully here.
156+
*
157+
* ```rb
158+
* class PostsController < ApplicationController
159+
* before_action :set_user
160+
* after_action :log_request
161+
* around_action :trace_request
162+
*
163+
* def index
164+
* @posts = @user.posts.all
165+
* end
166+
*
167+
* private
168+
*
169+
* def set_user
170+
* @user = User.find(session[:user_id])
171+
* end
172+
*
173+
* def log_request
174+
* Logger.info(request.path)
175+
* end
176+
*
177+
* def trace_request
178+
* start = Time.now
179+
* yield
180+
* Logger.info("Request took #{Time.now = start} seconds")
181+
* end
182+
* end
183+
* ```
184+
*/
185+
private class Filter extends StringlikeLiteralCfgNode {
186+
private FilterCall call;
187+
private FilterKind kind;
188+
189+
Filter() {
190+
this = call.getFilterArgument() and
191+
kind = call.getKind() and
192+
call.getMethodName() = ["", "prepend_", "append_"] + kind + "_action"
193+
}
194+
195+
predicate isPrepended() { call.getMethodName().regexpMatch("^prepend.+$") }
196+
197+
MethodCallCfgNode getCall() { result = call }
198+
199+
FilterKind getKind() { result = kind }
200+
201+
private ModuleBase getEnclosingModule() { result = call.getExpr().getEnclosingModule() }
202+
203+
/**
204+
* Holds if this callback is registered before `other`. This does not
205+
* guarantee that the callback will be executed before `other`. For example,
206+
* `after_action` callbacks are executed in reverse order.
207+
*/
208+
predicate registeredBefore(Filter other) {
209+
// before_action :this, :other
210+
//
211+
// before_action :this
212+
// before_action :other
213+
this.getBasicBlock() = other.getBasicBlock() and this.getASuccessor+() = other
214+
or
215+
this.getEnclosingModule() = other.getEnclosingModule() and
216+
this.getBasicBlock().strictlyDominates(other.getBasicBlock()) and
217+
this != other
218+
or
219+
// This callback is in a superclass of `other`'s class.
220+
//
221+
// class A
222+
// before_action :this
223+
//
224+
// class B < A
225+
// before_action :other
226+
other.getEnclosingModule().getModule() = this.getEnclosingModule().getModule().getASubClass+()
227+
}
228+
229+
/**
230+
* Holds if this callback runs before `other`.
231+
*/
232+
private predicate runsBefore(Filter other, ActionControllerActionMethod action) {
233+
other.getKind() = this.getKind() and
234+
not this.skipped(action) and
235+
not other.skipped(action) and
236+
action = this.getAction() and
237+
action = other.getAction() and
238+
(
239+
not this.isPrepended() and
240+
(
241+
not other.isPrepended() and
242+
(
243+
this.getKind() = TBeforeKind() and
244+
this.registeredBefore(other)
245+
or
246+
this.getKind() = TAfterKind() and
247+
other.registeredBefore(this)
248+
)
249+
or
250+
other.isPrepended() and this.getKind() = TAfterKind()
251+
)
252+
or
253+
this.isPrepended() and
254+
(
255+
not other.isPrepended() and
256+
this.getKind() = TBeforeKind()
257+
or
258+
other.isPrepended() and
259+
(
260+
this.getKind() = TBeforeKind() and this.registeredBefore(other)
261+
or
262+
this.getKind() = TAfterKind() and other.registeredBefore(this)
263+
)
264+
)
265+
)
266+
}
267+
268+
Filter getNextFilter(ActionControllerActionMethod action) {
269+
result != this and
270+
this.runsBefore(result, action) and
271+
not exists(Filter mid | this.runsBefore(mid, action) | mid.runsBefore(result, action))
272+
}
273+
274+
predicate skipped(ActionControllerActionMethod action) {
275+
this = any(SkipFilter f | f.getKind() = this.getKind()).getSkippedFilter(action)
276+
}
277+
278+
private string getFilterName() { result = this.getConstantValue().getStringlikeValue() }
279+
280+
Callable getFilterCallable() { result = call.getFilterCallable(this.getFilterName()) }
281+
282+
ActionControllerActionMethod getAction() { result = call.getAction() }
283+
}
284+
285+
private class BeforeFilter extends Filter {
286+
BeforeFilter() { this.getKind() = TBeforeKind() }
287+
}
288+
289+
private class AfterFilter extends Filter {
290+
AfterFilter() { this.getKind() = TAfterKind() }
291+
}
292+
293+
/**
294+
* A call to `skip_before_action`, `skip_after_action` or `skip_around_action`.
295+
* This skips a previously registered callback.
296+
* Like other filter calls, the `except` and `only` keyword arguments can be
297+
* passed to restrict the actions that the callback is skipped for.
298+
*/
299+
private class SkipFilter extends StringlikeLiteralCfgNode {
300+
private FilterCall call;
301+
private FilterKind kind;
302+
303+
SkipFilter() {
304+
this = call.getFilterArgument() and
305+
call.getMethodName() = "skip_" + kind + "_action"
306+
}
307+
308+
FilterKind getKind() { result = kind }
309+
310+
private string getFilterName() { result = this.getConstantValue().getStringlikeValue() }
311+
312+
Callable getFilterCallable() { result = call.getFilterCallable(this.getFilterName()) }
313+
314+
ActionControllerActionMethod getAction() { result = call.getAction() }
315+
316+
Filter getSkippedFilter(ActionControllerActionMethod action) {
317+
not result instanceof SkipFilter and
318+
action = this.getAction() and
319+
action = result.getAction() and
320+
result.getFilterCallable() = this.getFilterCallable()
321+
}
322+
}
323+
324+
/**
325+
* Holds if `pred` is called before `succ` in the callback chain for action `action`.
326+
* `pred` and `succ` may be methods bound to callbacks or controller actions.
327+
*/
328+
predicate next(ActionControllerActionMethod action, Method pred, Method succ) {
329+
exists(BeforeFilter f | pred = f.getFilterCallable() |
330+
// Non-terminal before filter
331+
succ = f.getNextFilter(action).getFilterCallable()
332+
or
333+
// Final before filter
334+
not exists(f.getNextFilter(action)) and
335+
not f.skipped(action) and
336+
action = f.getAction() and
337+
succ = action
338+
)
339+
or
340+
exists(AfterFilter f |
341+
// First after filter
342+
action = f.getAction() and
343+
not f.skipped(action) and
344+
pred = action and
345+
succ = f.getFilterCallable() and
346+
not exists(AfterFilter g | g.getNextFilter(action) = f)
347+
or
348+
// Subsequent after filter
349+
pred = f.getFilterCallable() and
350+
succ = f.getNextFilter(action).getFilterCallable()
351+
)
352+
}
353+
354+
/**
355+
* Holds if `pred` is called before `succ` in the callback chain for some action.
356+
* `pred` and `succ` may be methods bound to callbacks or controller actions.
357+
*/
358+
cached
359+
predicate next(Method pred, Method succ) {
360+
exists(ActionControllerActionMethod action | next(action, pred, succ))
361+
}
362+
}

0 commit comments

Comments
 (0)