Skip to content

Commit ee6c809

Browse files
authored
Merge pull request #262 from github/action-view-1
Start modelling ActionView
2 parents 348b12c + 9571e7b commit ee6c809

File tree

11 files changed

+427
-24
lines changed

11 files changed

+427
-24
lines changed

ql/src/codeql_ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44

55
private import codeql_ruby.frameworks.ActionController
66
private import codeql_ruby.frameworks.ActiveRecord
7+
private import codeql_ruby.frameworks.ActionView

ql/src/codeql_ruby/frameworks/ActionController.qll

Lines changed: 152 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ private import codeql_ruby.controlflow.CfgNodes
44
private import codeql_ruby.DataFlow
55
private import codeql_ruby.dataflow.RemoteFlowSources
66
private import codeql_ruby.ast.internal.Module
7+
private import ActionView
78

89
private class ActionControllerBaseAccess extends ConstantReadAccess {
910
ActionControllerBaseAccess() {
@@ -45,42 +46,175 @@ class ActionControllerControllerClass extends ClassDeclaration {
4546
other.getModule() = resolveScopeExpr(this.getSuperclassExpr())
4647
)
4748
}
49+
50+
/**
51+
* Gets a `ActionControllerActionMethod` defined in this class.
52+
*/
53+
ActionControllerActionMethod getAnAction() { result = this.getAMethod() }
4854
}
4955

5056
/**
51-
* A call to the `params` method within the context of an
52-
* `ActionControllerControllerClass`. For example, the `params` call in:
57+
* An instance method defined within an `ActionController` controller class.
58+
* This may be the target of a route handler, if such a route is defined.
59+
*/
60+
class ActionControllerActionMethod extends Method, HTTP::Server::RequestHandler::Range {
61+
private ActionControllerControllerClass controllerClass;
62+
63+
ActionControllerActionMethod() { this = controllerClass.getAMethod() }
64+
65+
/**
66+
* Establishes a mapping between a method within the file
67+
* `<sourcePrefix>app/controllers/<subpath>_controller.rb` and the
68+
* corresponding template file at
69+
* `<sourcePrefix>app/views/<subpath>/<method_name>.html.erb`.
70+
*/
71+
// TODO: result should be `ErbFile`
72+
File getDefaultTemplateFile() {
73+
controllerTemplatesFolder(this.getControllerClass(), result.getParentContainer()) and
74+
result.getBaseName() = this.getName() + ".html.erb"
75+
}
76+
77+
// params come from `params` method rather than a method parameter
78+
override Parameter getARoutedParameter() { none() }
79+
80+
override string getFramework() { result = "ActionController" }
81+
82+
/** Gets a call to render from within this method. */
83+
RenderCall getARenderCall() { result.getParent+() = this }
84+
85+
// TODO: model the implicit render call when a path through the method does
86+
// not end at an explicit render or redirect
87+
/** Gets the controller class containing this method. */
88+
ActionControllerControllerClass getControllerClass() { result = controllerClass }
89+
}
90+
91+
// A method call with a `self` receiver from within a controller class
92+
private class ActionControllerContextCall extends MethodCall {
93+
private ActionControllerControllerClass controllerClass;
94+
95+
ActionControllerContextCall() {
96+
this.getReceiver() instanceof Self and
97+
this.getEnclosingModule() = controllerClass
98+
}
99+
100+
ActionControllerControllerClass getControllerClass() { result = controllerClass }
101+
}
102+
103+
/**
104+
* A call to the `params` method to fetch the request parameters.
105+
*/
106+
abstract class ParamsCall extends MethodCall {
107+
ParamsCall() { this.getMethodName() = "params" }
108+
}
109+
110+
/**
111+
* A `RemoteFlowSource::Range` to represent accessing the
112+
* ActionController parameters available via the `params` method.
113+
*/
114+
class ParamsSource extends RemoteFlowSource::Range {
115+
ParamsCall call;
116+
117+
ParamsSource() { this.asExpr().getExpr() = call }
118+
119+
override string getSourceType() { result = "ActionController::Metal#params" }
120+
}
121+
122+
// A call to `params` from within a controller.
123+
private class ActionControllerParamsCall extends ActionControllerContextCall, ParamsCall { }
124+
125+
// A call to `render` from within a controller.
126+
private class ActionControllerRenderCall extends ActionControllerContextCall, RenderCall { }
127+
128+
// A call to `render_to` from within a controller.
129+
private class ActionControllerRenderToCall extends ActionControllerContextCall, RenderToCall { }
130+
131+
// A call to `html_safe` from within a controller.
132+
private class ActionControllerHtmlSafeCall extends HtmlSafeCall {
133+
ActionControllerHtmlSafeCall() {
134+
this.getEnclosingModule() instanceof ActionControllerControllerClass
135+
}
136+
}
137+
138+
/**
139+
* A call to the `redirect_to` method, used in an action to redirect to a
140+
* specific URL/path or to a different action in this controller.
141+
*/
142+
class RedirectToCall extends ActionControllerContextCall {
143+
RedirectToCall() { this.getMethodName() = "redirect_to" }
144+
145+
/** Gets the `Expr` representing the URL to redirect to, if any */
146+
Expr getRedirectUrl() { result = this.getArgument(0) }
147+
148+
/** Gets the `ActionControllerActionMethod` to redirect to, if any */
149+
ActionControllerActionMethod getRedirectActionMethod() {
150+
exists(string methodName |
151+
methodName = this.getKeywordArgument("action").(StringlikeLiteral).getValueText() and
152+
methodName = result.getName() and
153+
result.getEnclosingModule() = this.getControllerClass()
154+
)
155+
}
156+
}
157+
158+
/**
159+
* A method in an `ActionController` class that is accessible from within a
160+
* Rails view as a helper method. For instance, in:
53161
*
54162
* ```rb
55163
* class FooController < ActionController::Base
56-
* def delete_handler
57-
* uid = params[:id]
58-
* User.delete_all("id = ?", uid)
164+
* helper_method :logged_in?
165+
* def logged_in?
166+
* @current_user != nil
59167
* end
60168
* end
61169
* ```
170+
*
171+
* the `logged_in?` method is a helper method.
172+
* See also https://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper_method
62173
*/
63-
class ActionControllerParamsCall extends MethodCall {
174+
class ActionControllerHelperMethod extends Method {
64175
private ActionControllerControllerClass controllerClass;
65176

66-
ActionControllerParamsCall() {
67-
this.getMethodName() = "params" and
68-
this.getReceiver() instanceof Self and
69-
this.getEnclosingModule() = controllerClass
177+
ActionControllerHelperMethod() {
178+
this.getEnclosingModule() = controllerClass and
179+
exists(MethodCall helperMethodMarker |
180+
helperMethodMarker.getMethodName() = "helper_method" and
181+
helperMethodMarker.getAnArgument().(StringlikeLiteral).getValueText() = this.getName() and
182+
helperMethodMarker.getEnclosingModule() = controllerClass
183+
)
70184
}
71185

186+
/** Gets the class containing this helper method. */
72187
ActionControllerControllerClass getControllerClass() { result = controllerClass }
73188
}
74189

75190
/**
76-
* A `RemoteFlowSource::Range` to represent accessing the Action Controller
77-
* parameters available to a controller via the `params` method.
191+
* Gets an `ActionControllerControllerClass` associated with the given `ErbFile`
192+
* according to Rails path conventions.
193+
* For instance, a template file at `app/views/foo/bar/baz.html.erb` will be
194+
* mapped to a controller class in `app/controllers/foo/bar/baz_controller.rb`,
195+
* if such a controller class exists.
78196
*/
79-
class ActionControllerParamsSource extends RemoteFlowSource::Range {
80-
ActionControllerParamsCall call;
81-
82-
ActionControllerParamsSource() { this.asExpr().getExpr() = call }
197+
// TODO: parameter should be `ErbFile`
198+
ActionControllerControllerClass getAssociatedControllerClass(File f) {
199+
controllerTemplatesFolder(result, f.getParentContainer())
200+
}
83201

84-
// TODO: what to use here?
85-
override string getSourceType() { result = "ActionController::Metal#params" }
202+
/**
203+
* Holds if `templatesFolder` is in the correct location to contain template
204+
* files "belonging" to the given `ActionControllerControllerClass`, according
205+
* to Rails conventions.
206+
*
207+
* In particular, this means that an action method in `cls` will by default
208+
* render a correspondingly named template file within `templatesFolder`.
209+
*/
210+
predicate controllerTemplatesFolder(ActionControllerControllerClass cls, Folder templatesFolder) {
211+
exists(string templatesPath, string sourcePrefix, string subPath, string controllerPath |
212+
controllerPath = cls.getLocation().getFile().getRelativePath() and
213+
templatesPath = templatesFolder.getRelativePath() and
214+
// `sourcePrefix` is either a prefix path ending in a slash, or empty if
215+
// the rails app is at the source root
216+
sourcePrefix = [controllerPath.regexpCapture("^(.*/)app/controllers/(?:.*?)/(?:[^/]*)$", 1), ""] and
217+
controllerPath = sourcePrefix + "app/controllers/" + subPath + "_controller.rb" and
218+
templatesPath = sourcePrefix + "app/views/" + subPath
219+
)
86220
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
private import codeql_ruby.AST
2+
private import codeql_ruby.Concepts
3+
private import codeql_ruby.controlflow.CfgNodes
4+
private import codeql_ruby.DataFlow
5+
private import codeql_ruby.dataflow.RemoteFlowSources
6+
private import codeql_ruby.ast.internal.Module
7+
private import ActionController
8+
9+
predicate inActionViewContext(AstNode n) {
10+
// Within a template
11+
// TODO: n.getLocation().getFile() instanceof ErbFile
12+
n.getLocation().getFile().getExtension() = "erb"
13+
}
14+
15+
/**
16+
* A method call on a string to mark it as HTML safe for Rails.
17+
* Strings marked as such will not be automatically escaped when inserted into
18+
* HTML.
19+
*/
20+
abstract class HtmlSafeCall extends MethodCall {
21+
HtmlSafeCall() { this.getMethodName() = "html_safe" }
22+
}
23+
24+
// A call to `html_safe` from within a template or view component.
25+
private class ActionViewHtmlSafeCall extends HtmlSafeCall {
26+
ActionViewHtmlSafeCall() { inActionViewContext(this) }
27+
}
28+
29+
// A call in a context where some commonly used `ActionView` methods are available.
30+
private class ActionViewContextCall extends MethodCall {
31+
ActionViewContextCall() {
32+
this.getReceiver() instanceof Self and
33+
inActionViewContext(this)
34+
}
35+
36+
predicate isInErbFile() {
37+
// TODO: this.getLocation().getFile() instanceof ErbFile
38+
this.getLocation().getFile().getExtension() = "erb"
39+
}
40+
}
41+
42+
/** A call to the `raw` method to output a value without HTML escaping. */
43+
class RawCall extends ActionViewContextCall {
44+
RawCall() { this.getMethodName() = "raw" }
45+
}
46+
47+
// A call to the `params` method within the context of a template or view component.
48+
private class ActionViewParamsCall extends ActionViewContextCall, ParamsCall { }
49+
50+
/**
51+
* A call to a `render` method that will populate the response body with the
52+
* rendered content.
53+
*/
54+
abstract class RenderCall extends MethodCall {
55+
RenderCall() { this.getMethodName() = "render" }
56+
57+
private string getWorkingDirectory() {
58+
result = this.getLocation().getFile().getParentContainer().getAbsolutePath()
59+
}
60+
61+
bindingset[templatePath]
62+
private string templatePathPattern(string templatePath) {
63+
exists(string basename, string relativeRoot |
64+
// everything after the final slash, or the whole string if there is no slash
65+
basename = templatePath.regexpCapture("^(?:.*/)?([^/]*)$", 1) and
66+
// everything up to and including the final slash
67+
relativeRoot = templatePath.regexpCapture("^(.*/)?(?:[^/]*?)$", 1)
68+
|
69+
(
70+
// path relative to <source_prefix>/app/views/
71+
result = "%/app/views/" + relativeRoot + "%" + basename + "%"
72+
or
73+
// relative to file containing call
74+
result = this.getWorkingDirectory() + "%" + templatePath + "%"
75+
)
76+
)
77+
}
78+
79+
private string getTemplatePathPatterns() {
80+
exists(string templatePath |
81+
exists(Expr arg |
82+
// TODO: support other ways of specifying paths (e.g. `file`)
83+
arg = this.getKeywordArgument("partial") or
84+
arg = this.getKeywordArgument("template") or
85+
arg = this.getKeywordArgument("action") or
86+
arg = this.getArgument(0)
87+
|
88+
templatePath = arg.(StringlikeLiteral).getValueText()
89+
)
90+
|
91+
result = this.templatePathPattern(templatePath)
92+
)
93+
}
94+
95+
/**
96+
* Get the template file to be rendered by this call, if any.
97+
*/
98+
// TODO: parameter should be `ErbFile`
99+
File getTemplateFile() { result.getAbsolutePath().matches(this.getTemplatePathPatterns()) }
100+
101+
/**
102+
* Get the local variables passed as context to the renderer
103+
*/
104+
HashLiteral getLocals() { result = this.getKeywordArgument("locals") }
105+
// TODO: implicit renders in controller actions
106+
}
107+
108+
// A call to the `render` method within the context of a template or view component.
109+
private class ActionViewRenderCall extends RenderCall, ActionViewContextCall { }
110+
111+
/**
112+
* A render call that does not automatically set the HTTP response body.
113+
*/
114+
abstract class RenderToCall extends MethodCall {
115+
RenderToCall() { this.getMethodName() = ["render_to_body", "render_to_string"] }
116+
}
117+
118+
// A call to `render_to` from within a template or view component.
119+
private class ActionViewRenderToCall extends ActionViewContextCall, RenderToCall { }
120+
121+
/**
122+
* A call to the ActionView `link_to` helper method.
123+
*
124+
* This generates an HTML anchor tag. The method is not designed to expect
125+
* user-input, so provided paths are not automatically HTML escaped.
126+
*/
127+
class LinkToCall extends ActionViewContextCall {
128+
LinkToCall() { this.getMethodName() = "link_to" }
129+
130+
// TODO: the path can also be specified through other optional arguments
131+
Expr getPathArgument() { result = this.getArgument(1) }
132+
}
133+
// TODO: model flow in/out of template files properly,

ql/test/library-tests/frameworks/ActionController.expected

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ actionControllerControllerClasses
22
| ActiveRecordInjection.rb:27:1:58:3 | FooController |
33
| ActiveRecordInjection.rb:60:1:90:3 | BarController |
44
| ActiveRecordInjection.rb:92:1:96:3 | BazController |
5-
actionControllerParamsCalls
5+
| app/controllers/foo/bars_controller.rb:1:1:20:3 | BarsController |
6+
actionControllerActionMethods
7+
| ActiveRecordInjection.rb:32:3:57:5 | some_request_handler |
8+
| ActiveRecordInjection.rb:61:3:69:5 | some_other_request_handler |
9+
| ActiveRecordInjection.rb:71:3:89:5 | safe_paths |
10+
| ActiveRecordInjection.rb:93:3:95:5 | yet_another_handler |
11+
| app/controllers/foo/bars_controller.rb:3:3:5:5 | index |
12+
| app/controllers/foo/bars_controller.rb:7:3:13:5 | show_debug |
13+
| app/controllers/foo/bars_controller.rb:15:3:19:5 | show |
14+
paramsCalls
615
| ActiveRecordInjection.rb:35:30:35:35 | call to params |
716
| ActiveRecordInjection.rb:39:30:39:35 | call to params |
817
| ActiveRecordInjection.rb:43:32:43:37 | call to params |
@@ -16,7 +25,12 @@ actionControllerParamsCalls
1625
| ActiveRecordInjection.rb:83:12:83:17 | call to params |
1726
| ActiveRecordInjection.rb:88:15:88:20 | call to params |
1827
| ActiveRecordInjection.rb:94:22:94:27 | call to params |
19-
actionControllerParamsSources
28+
| app/controllers/foo/bars_controller.rb:8:21:8:26 | call to params |
29+
| app/controllers/foo/bars_controller.rb:9:10:9:15 | call to params |
30+
| app/controllers/foo/bars_controller.rb:16:21:16:26 | call to params |
31+
| app/controllers/foo/bars_controller.rb:17:10:17:15 | call to params |
32+
| app/views/foo/bars/show.html.erb:5:9:5:14 | call to params |
33+
paramsSources
2034
| ActiveRecordInjection.rb:35:30:35:35 | call to params |
2135
| ActiveRecordInjection.rb:39:30:39:35 | call to params |
2236
| ActiveRecordInjection.rb:43:32:43:37 | call to params |
@@ -30,3 +44,16 @@ actionControllerParamsSources
3044
| ActiveRecordInjection.rb:83:12:83:17 | call to params |
3145
| ActiveRecordInjection.rb:88:15:88:20 | call to params |
3246
| ActiveRecordInjection.rb:94:22:94:27 | call to params |
47+
| app/controllers/foo/bars_controller.rb:8:21:8:26 | call to params |
48+
| app/controllers/foo/bars_controller.rb:9:10:9:15 | call to params |
49+
| app/controllers/foo/bars_controller.rb:16:21:16:26 | call to params |
50+
| app/controllers/foo/bars_controller.rb:17:10:17:15 | call to params |
51+
| app/views/foo/bars/show.html.erb:5:9:5:14 | call to params |
52+
redirectToCalls
53+
| app/controllers/foo/bars_controller.rb:12:5:12:30 | call to redirect_to |
54+
actionControllerHelperMethods
55+
getAssociatedControllerClasses
56+
| app/controllers/foo/bars_controller.rb:1:1:20:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
57+
| app/controllers/foo/bars_controller.rb:1:1:20:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
58+
controllerTemplatesFolders
59+
| app/controllers/foo/bars_controller.rb:1:1:20:3 | BarsController | folder://app/views/foo/bars | app/views/foo/bars |

0 commit comments

Comments
 (0)