Skip to content

Commit 41ff10c

Browse files
committed
extend modelling of ActionController, and start modelling ActionView
1 parent 9c17e00 commit 41ff10c

5 files changed

Lines changed: 270 additions & 54 deletions

File tree

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: 116 additions & 20 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,26 +46,58 @@ 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:
53-
*
54-
* ```rb
55-
* class FooController < ActionController::Base
56-
* def delete_handler
57-
* uid = params[:id]
58-
* User.delete_all("id = ?", uid)
59-
* end
60-
* end
61-
* ```
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.
6259
*/
63-
class ActionControllerParamsCall extends MethodCall {
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+
* `<source_prefix>/app/controllers/<subpath>_controller.rb` and the
68+
* corresponding template file at
69+
* `<source_prefix>/app/views/<subpath>/<method_name>.html.erb`.
70+
*/
71+
ErbFile getDefaultTemplateFile() {
72+
exists(string templatePath, string sourcePrefix, string subPath, string controllerPath |
73+
controllerPath = this.getLocation().getFile().getAbsolutePath() and
74+
sourcePrefix = controllerPath.regexpCapture("^(.*)/app/controllers/.*$", 1) and
75+
controllerPath = sourcePrefix + "/app/controllers/" + subPath + "_controller.rb" and
76+
templatePath = sourcePrefix + "/app/views/" + subPath + "/" + this.getName() + ".html.erb"
77+
|
78+
result.getAbsolutePath() = templatePath
79+
)
80+
}
81+
82+
// params come from `params` method rather than a method parameter
83+
override Parameter getARoutedParameter() { none() }
84+
85+
override string getFramework() { result = "ActionController" }
86+
87+
/** Gets a call to render from within this method. */
88+
RenderCall getARenderCall() { result.getParent+() = this }
89+
90+
// TODO: model the implicit render call when a path through the method does
91+
// not end at an explicit render or redirect
92+
/** Gets the controller class containing this method. */
93+
ActionControllerControllerClass getControllerClass() { result = controllerClass }
94+
}
95+
96+
// A method call with a `self` receiver from within a controller class
97+
private class ActionControllerContextCall extends MethodCall {
6498
private ActionControllerControllerClass controllerClass;
6599

66-
ActionControllerParamsCall() {
67-
this.getMethodName() = "params" and
100+
ActionControllerContextCall() {
68101
this.getReceiver() instanceof Self and
69102
this.getEnclosingModule() = controllerClass
70103
}
@@ -73,14 +106,77 @@ class ActionControllerParamsCall extends MethodCall {
73106
}
74107

75108
/**
76-
* A `RemoteFlowSource::Range` to represent accessing the Action Controller
77-
* parameters available to a controller via the `params` method.
109+
* A call to the `params` method to fetch the request parameters.
110+
*/
111+
abstract class ParamsCall extends MethodCall {
112+
ParamsCall() { this.getMethodName() = "params" }
113+
}
114+
115+
/**
116+
* A `RemoteFlowSource::Range` to represent accessing the
117+
* ActionController parameters available via the `params` method.
78118
*/
79-
class ActionControllerParamsSource extends RemoteFlowSource::Range {
80-
ActionControllerParamsCall call;
119+
class ParamsSource extends RemoteFlowSource::Range {
120+
ParamsCall call;
81121

82-
ActionControllerParamsSource() { this.asExpr().getExpr() = call }
122+
ParamsSource() { this.asExpr().getExpr() = call }
83123

84-
// TODO: what to use here?
85124
override string getSourceType() { result = "ActionController::Metal#params" }
86125
}
126+
127+
// A call to `params` from within a controller.
128+
private class ActionControllerParamsCall extends ActionControllerContextCall, ParamsCall { }
129+
130+
// A call to `render_to` from within a controller.
131+
private class ActionControllerRenderToCall extends ActionControllerContextCall, RenderToCall { }
132+
133+
// A call to `html_safe` from within a controller.
134+
private class ActionControllerHtmlSafeCall extends HtmlSafeCall {
135+
ActionControllerHtmlSafeCall() { this.getEnclosingModule() instanceof ActionControllerControllerClass }
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 `SetterMethodCall` that assigns a value to the `response_body`.
160+
*/
161+
class ResponseBodySetterCall extends SetterMethodCall {
162+
ResponseBodySetterCall() { this.getMethodName() = "response_body=" }
163+
}
164+
165+
/**
166+
* A method in an `ActionController` class that is accessible from within a view as a helper method.
167+
*/
168+
class ActionControllerHelperMethod extends Method {
169+
private ActionControllerControllerClass controllerClass;
170+
171+
ActionControllerHelperMethod() {
172+
this.getEnclosingModule() = controllerClass and
173+
exists(MethodCall helperMethodMarker |
174+
helperMethodMarker.getMethodName() = "helper_method" and
175+
helperMethodMarker.getAnArgument().(StringlikeLiteral).getValueText() = this.getName() and
176+
helperMethodMarker.getEnclosingModule() = controllerClass
177+
)
178+
}
179+
180+
/** Gets the class containing this helper method. */
181+
ActionControllerControllerClass getControllerClass() { result = controllerClass }
182+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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 view component
11+
n.getEnclosingModule() instanceof ViewComponentClass
12+
or
13+
// Within a template
14+
n.getLocation().getFile() instanceof ErbFile
15+
}
16+
17+
/**
18+
* A method call on a string to mark it as HTML safe for Rails.
19+
* Strings marked as such will not be automatically escaped when inserted into
20+
* HTML.
21+
*/
22+
abstract class HtmlSafeCall extends MethodCall {
23+
HtmlSafeCall() { this.getMethodName() = "html_safe" }
24+
}
25+
26+
// A call to `html_safe` from within a template or view component.
27+
private class ActionViewHtmlSafeCall extends HtmlSafeCall {
28+
ActionViewHtmlSafeCall() { inActionViewContext(this) }
29+
}
30+
31+
// A call in a context where some commonly used `ActionView` methods are available.
32+
private class ActionViewContextCall extends MethodCall {
33+
ActionViewContextCall() {
34+
this.getReceiver() instanceof Self and
35+
inActionViewContext(this)
36+
}
37+
38+
predicate isInErbFile() { this.getLocation().getFile() instanceof ErbFile }
39+
}
40+
41+
/** A call to the `raw` method to output a value without HTML escaping. */
42+
class RawCall extends ActionViewContextCall {
43+
RawCall() { this.getMethodName() = "raw" }
44+
}
45+
46+
/**
47+
* A call to the `params` method within the context of a template or view component.
48+
*/
49+
private class ActionViewParamsCall extends ActionViewContextCall, ParamsCall { }
50+
51+
/**
52+
* A call to a `render` method that will populate the response body with the
53+
* rendered content.
54+
*/
55+
class RenderCall extends ActionViewContextCall {
56+
RenderCall() { this.getMethodName() = "render" }
57+
58+
private string getWorkingDirectory() {
59+
result = this.getLocation().getFile().getParentContainer().getAbsolutePath()
60+
}
61+
62+
bindingset[templatePath]
63+
private string templatePathPattern(string templatePath) {
64+
exists(string basename, string relativeRoot |
65+
// everything after the final slash, or the whole string if there is no slash
66+
basename = templatePath.regexpCapture("^(?:.*/)?([^/]*)$", 1) and
67+
// everything up to and including the final slash
68+
relativeRoot = templatePath.regexpCapture("^(.*/)?(?:[^/]*?)$", 1)
69+
|
70+
(
71+
// path relative to <source_prefix>/app/views/
72+
result = "%/app/views/" + relativeRoot + "%" + basename + "%"
73+
or
74+
// relative to file containing call
75+
result = this.getWorkingDirectory() + "%" + templatePath + "%"
76+
)
77+
)
78+
}
79+
80+
private string getTemplatePathPatterns() {
81+
exists(string templatePath |
82+
exists(Expr arg |
83+
// TODO: support other ways of specifying paths (e.g. `file`)
84+
arg = this.getKeywordArgument("partial") or
85+
arg = this.getKeywordArgument("template") or
86+
arg = this.getKeywordArgument("action") or
87+
arg = this.getArgument(0)
88+
|
89+
templatePath = arg.(StringlikeLiteral).getValueText()
90+
)
91+
|
92+
result = this.templatePathPattern(templatePath)
93+
)
94+
}
95+
96+
/**
97+
* Get the template file to be rendered by this call, if any.
98+
*/
99+
ErbFile getTemplate() { 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+
/**
109+
* A render call that does not automatically set the HTTP response body.
110+
*/
111+
abstract class RenderToCall extends MethodCall {
112+
RenderToCall() { this.getMethodName() = ["render_to_body", "render_to_string"] }
113+
}
114+
115+
// A call to `render_to` from within a template or view component.
116+
private class ActionViewRenderToCall extends ActionViewContextCall, RenderToCall { }
117+
118+
private class ViewComponentBaseAccess extends ConstantReadAccess {
119+
ViewComponentBaseAccess() {
120+
this.getName() = "Base" and
121+
this.getScopeExpr().(ConstantAccess).getName() = "ViewComponent"
122+
}
123+
}
124+
125+
/**
126+
* A class extending `ViewComponent::Base`.
127+
*/
128+
class ViewComponentClass extends ClassDeclaration {
129+
ViewComponentClass() {
130+
// class Foo < ViewComponent::Base
131+
this.getSuperclassExpr() instanceof ViewComponentBaseAccess
132+
or
133+
// class Bar < Foo
134+
exists(ViewComponentClass other |
135+
other.getModule() = resolveScopeExpr(this.getSuperclassExpr())
136+
)
137+
}
138+
}
139+
140+
/**
141+
* A call to the ActionView `link_to` helper method.
142+
*
143+
* This generates an HTML anchor tag. The method is not designed to expect
144+
* user-input, so provided paths are not automatically HTML escaped.
145+
*/
146+
class LinkToCall extends ActionViewContextCall {
147+
LinkToCall() { this.getMethodName() = "link_to" }
148+
149+
// TODO: the path can also be specified through other optional arguments
150+
Expr getPathArgument() { result = this.getArgument(1) }
151+
}
152+
// TODO: model flow in/out of template files properly,
Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,3 @@
1-
actionControllerControllerClasses
21
| ActiveRecordInjection.rb:27:1:58:3 | FooController |
32
| ActiveRecordInjection.rb:60:1:90:3 | BarController |
43
| ActiveRecordInjection.rb:92:1:96:3 | BazController |
5-
actionControllerParamsCalls
6-
| ActiveRecordInjection.rb:35:30:35:35 | call to params |
7-
| ActiveRecordInjection.rb:39:30:39:35 | call to params |
8-
| ActiveRecordInjection.rb:43:32:43:37 | call to params |
9-
| ActiveRecordInjection.rb:48:21:48:26 | call to params |
10-
| ActiveRecordInjection.rb:54:34:54:39 | call to params |
11-
| ActiveRecordInjection.rb:56:23:56:28 | call to params |
12-
| ActiveRecordInjection.rb:56:38:56:43 | call to params |
13-
| ActiveRecordInjection.rb:62:10:62:15 | call to params |
14-
| ActiveRecordInjection.rb:72:11:72:16 | call to params |
15-
| ActiveRecordInjection.rb:77:12:77:17 | call to params |
16-
| ActiveRecordInjection.rb:83:12:83:17 | call to params |
17-
| ActiveRecordInjection.rb:88:15:88:20 | call to params |
18-
| ActiveRecordInjection.rb:94:22:94:27 | call to params |
19-
actionControllerParamsSources
20-
| ActiveRecordInjection.rb:35:30:35:35 | call to params |
21-
| ActiveRecordInjection.rb:39:30:39:35 | call to params |
22-
| ActiveRecordInjection.rb:43:32:43:37 | call to params |
23-
| ActiveRecordInjection.rb:48:21:48:26 | call to params |
24-
| ActiveRecordInjection.rb:54:34:54:39 | call to params |
25-
| ActiveRecordInjection.rb:56:23:56:28 | call to params |
26-
| ActiveRecordInjection.rb:56:38:56:43 | call to params |
27-
| ActiveRecordInjection.rb:62:10:62:15 | call to params |
28-
| ActiveRecordInjection.rb:72:11:72:16 | call to params |
29-
| ActiveRecordInjection.rb:77:12:77:17 | call to params |
30-
| ActiveRecordInjection.rb:83:12:83:17 | call to params |
31-
| ActiveRecordInjection.rb:88:15:88:20 | call to params |
32-
| ActiveRecordInjection.rb:94:22:94:27 | call to params |
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
import codeql_ruby.controlflow.CfgNodes
22
import codeql_ruby.frameworks.ActionController
33

4-
query predicate actionControllerControllerClasses(ActionControllerControllerClass cls) { any() }
5-
6-
query predicate actionControllerParamsCalls(ActionControllerParamsCall call) { any() }
7-
8-
query predicate actionControllerParamsSources(ActionControllerParamsSource source) { any() }
4+
query predicate actionControllerControllerClasses(ActionControllerControllerClass cls) { any() }

0 commit comments

Comments
 (0)