extend modelling of ActionController, and start modelling ActionView

This commit is contained in:
Alex Ford
2021-08-15 22:51:36 +01:00
parent 9c17e00645
commit 41ff10c908
5 changed files with 270 additions and 54 deletions

View File

@@ -4,3 +4,4 @@
private import codeql_ruby.frameworks.ActionController
private import codeql_ruby.frameworks.ActiveRecord
private import codeql_ruby.frameworks.ActionView

View File

@@ -4,6 +4,7 @@ private import codeql_ruby.controlflow.CfgNodes
private import codeql_ruby.DataFlow
private import codeql_ruby.dataflow.RemoteFlowSources
private import codeql_ruby.ast.internal.Module
private import ActionView
private class ActionControllerBaseAccess extends ConstantReadAccess {
ActionControllerBaseAccess() {
@@ -45,26 +46,58 @@ class ActionControllerControllerClass extends ClassDeclaration {
other.getModule() = resolveScopeExpr(this.getSuperclassExpr())
)
}
/**
* Gets a `ActionControllerActionMethod` defined in this class.
*/
ActionControllerActionMethod getAnAction() { result = this.getAMethod() }
}
/**
* A call to the `params` method within the context of an
* `ActionControllerControllerClass`. For example, the `params` call in:
*
* ```rb
* class FooController < ActionController::Base
* def delete_handler
* uid = params[:id]
* User.delete_all("id = ?", uid)
* end
* end
* ```
* An instance method defined within an `ActionController` controller class.
* This may be the target of a route handler, if such a route is defined.
*/
class ActionControllerParamsCall extends MethodCall {
class ActionControllerActionMethod extends Method, HTTP::Server::RequestHandler::Range {
private ActionControllerControllerClass controllerClass;
ActionControllerParamsCall() {
this.getMethodName() = "params" and
ActionControllerActionMethod() { this = controllerClass.getAMethod() }
/**
* Establishes a mapping between a method within the file
* `<source_prefix>/app/controllers/<subpath>_controller.rb` and the
* corresponding template file at
* `<source_prefix>/app/views/<subpath>/<method_name>.html.erb`.
*/
ErbFile getDefaultTemplateFile() {
exists(string templatePath, string sourcePrefix, string subPath, string controllerPath |
controllerPath = this.getLocation().getFile().getAbsolutePath() and
sourcePrefix = controllerPath.regexpCapture("^(.*)/app/controllers/.*$", 1) and
controllerPath = sourcePrefix + "/app/controllers/" + subPath + "_controller.rb" and
templatePath = sourcePrefix + "/app/views/" + subPath + "/" + this.getName() + ".html.erb"
|
result.getAbsolutePath() = templatePath
)
}
// params come from `params` method rather than a method parameter
override Parameter getARoutedParameter() { none() }
override string getFramework() { result = "ActionController" }
/** Gets a call to render from within this method. */
RenderCall getARenderCall() { result.getParent+() = this }
// TODO: model the implicit render call when a path through the method does
// not end at an explicit render or redirect
/** Gets the controller class containing this method. */
ActionControllerControllerClass getControllerClass() { result = controllerClass }
}
// A method call with a `self` receiver from within a controller class
private class ActionControllerContextCall extends MethodCall {
private ActionControllerControllerClass controllerClass;
ActionControllerContextCall() {
this.getReceiver() instanceof Self and
this.getEnclosingModule() = controllerClass
}
@@ -73,14 +106,77 @@ class ActionControllerParamsCall extends MethodCall {
}
/**
* A `RemoteFlowSource::Range` to represent accessing the Action Controller
* parameters available to a controller via the `params` method.
* A call to the `params` method to fetch the request parameters.
*/
class ActionControllerParamsSource extends RemoteFlowSource::Range {
ActionControllerParamsCall call;
abstract class ParamsCall extends MethodCall {
ParamsCall() { this.getMethodName() = "params" }
}
ActionControllerParamsSource() { this.asExpr().getExpr() = call }
/**
* A `RemoteFlowSource::Range` to represent accessing the
* ActionController parameters available via the `params` method.
*/
class ParamsSource extends RemoteFlowSource::Range {
ParamsCall call;
ParamsSource() { this.asExpr().getExpr() = call }
// TODO: what to use here?
override string getSourceType() { result = "ActionController::Metal#params" }
}
// A call to `params` from within a controller.
private class ActionControllerParamsCall extends ActionControllerContextCall, ParamsCall { }
// A call to `render_to` from within a controller.
private class ActionControllerRenderToCall extends ActionControllerContextCall, RenderToCall { }
// A call to `html_safe` from within a controller.
private class ActionControllerHtmlSafeCall extends HtmlSafeCall {
ActionControllerHtmlSafeCall() { this.getEnclosingModule() instanceof ActionControllerControllerClass }
}
/**
* A call to the `redirect_to` method, used in an action to redirect to a
* specific URL/path or to a different action in this controller.
*/
class RedirectToCall extends ActionControllerContextCall {
RedirectToCall() { this.getMethodName() = "redirect_to" }
/** Gets the `Expr` representing the URL to redirect to, if any */
Expr getRedirectUrl() { result = this.getArgument(0) }
/** Gets the `ActionControllerActionMethod` to redirect to, if any */
ActionControllerActionMethod getRedirectActionMethod() {
exists(string methodName |
methodName = this.getKeywordArgument("action").(StringlikeLiteral).getValueText() and
methodName = result.getName() and
result.getEnclosingModule() = this.getControllerClass()
)
}
}
/**
* A `SetterMethodCall` that assigns a value to the `response_body`.
*/
class ResponseBodySetterCall extends SetterMethodCall {
ResponseBodySetterCall() { this.getMethodName() = "response_body=" }
}
/**
* A method in an `ActionController` class that is accessible from within a view as a helper method.
*/
class ActionControllerHelperMethod extends Method {
private ActionControllerControllerClass controllerClass;
ActionControllerHelperMethod() {
this.getEnclosingModule() = controllerClass and
exists(MethodCall helperMethodMarker |
helperMethodMarker.getMethodName() = "helper_method" and
helperMethodMarker.getAnArgument().(StringlikeLiteral).getValueText() = this.getName() and
helperMethodMarker.getEnclosingModule() = controllerClass
)
}
/** Gets the class containing this helper method. */
ActionControllerControllerClass getControllerClass() { result = controllerClass }
}

View File

@@ -0,0 +1,152 @@
private import codeql_ruby.AST
private import codeql_ruby.Concepts
private import codeql_ruby.controlflow.CfgNodes
private import codeql_ruby.DataFlow
private import codeql_ruby.dataflow.RemoteFlowSources
private import codeql_ruby.ast.internal.Module
private import ActionController
predicate inActionViewContext(AstNode n) {
// Within a view component
n.getEnclosingModule() instanceof ViewComponentClass
or
// Within a template
n.getLocation().getFile() instanceof ErbFile
}
/**
* A method call on a string to mark it as HTML safe for Rails.
* Strings marked as such will not be automatically escaped when inserted into
* HTML.
*/
abstract class HtmlSafeCall extends MethodCall {
HtmlSafeCall() { this.getMethodName() = "html_safe" }
}
// A call to `html_safe` from within a template or view component.
private class ActionViewHtmlSafeCall extends HtmlSafeCall {
ActionViewHtmlSafeCall() { inActionViewContext(this) }
}
// A call in a context where some commonly used `ActionView` methods are available.
private class ActionViewContextCall extends MethodCall {
ActionViewContextCall() {
this.getReceiver() instanceof Self and
inActionViewContext(this)
}
predicate isInErbFile() { this.getLocation().getFile() instanceof ErbFile }
}
/** A call to the `raw` method to output a value without HTML escaping. */
class RawCall extends ActionViewContextCall {
RawCall() { this.getMethodName() = "raw" }
}
/**
* A call to the `params` method within the context of a template or view component.
*/
private class ActionViewParamsCall extends ActionViewContextCall, ParamsCall { }
/**
* A call to a `render` method that will populate the response body with the
* rendered content.
*/
class RenderCall extends ActionViewContextCall {
RenderCall() { this.getMethodName() = "render" }
private string getWorkingDirectory() {
result = this.getLocation().getFile().getParentContainer().getAbsolutePath()
}
bindingset[templatePath]
private string templatePathPattern(string templatePath) {
exists(string basename, string relativeRoot |
// everything after the final slash, or the whole string if there is no slash
basename = templatePath.regexpCapture("^(?:.*/)?([^/]*)$", 1) and
// everything up to and including the final slash
relativeRoot = templatePath.regexpCapture("^(.*/)?(?:[^/]*?)$", 1)
|
(
// path relative to <source_prefix>/app/views/
result = "%/app/views/" + relativeRoot + "%" + basename + "%"
or
// relative to file containing call
result = this.getWorkingDirectory() + "%" + templatePath + "%"
)
)
}
private string getTemplatePathPatterns() {
exists(string templatePath |
exists(Expr arg |
// TODO: support other ways of specifying paths (e.g. `file`)
arg = this.getKeywordArgument("partial") or
arg = this.getKeywordArgument("template") or
arg = this.getKeywordArgument("action") or
arg = this.getArgument(0)
|
templatePath = arg.(StringlikeLiteral).getValueText()
)
|
result = this.templatePathPattern(templatePath)
)
}
/**
* Get the template file to be rendered by this call, if any.
*/
ErbFile getTemplate() { result.getAbsolutePath().matches(this.getTemplatePathPatterns()) }
/**
* Get the local variables passed as context to the renderer
*/
HashLiteral getLocals() { result = this.getKeywordArgument("locals") }
// TODO: implicit renders in controller actions
}
/**
* A render call that does not automatically set the HTTP response body.
*/
abstract class RenderToCall extends MethodCall {
RenderToCall() { this.getMethodName() = ["render_to_body", "render_to_string"] }
}
// A call to `render_to` from within a template or view component.
private class ActionViewRenderToCall extends ActionViewContextCall, RenderToCall { }
private class ViewComponentBaseAccess extends ConstantReadAccess {
ViewComponentBaseAccess() {
this.getName() = "Base" and
this.getScopeExpr().(ConstantAccess).getName() = "ViewComponent"
}
}
/**
* A class extending `ViewComponent::Base`.
*/
class ViewComponentClass extends ClassDeclaration {
ViewComponentClass() {
// class Foo < ViewComponent::Base
this.getSuperclassExpr() instanceof ViewComponentBaseAccess
or
// class Bar < Foo
exists(ViewComponentClass other |
other.getModule() = resolveScopeExpr(this.getSuperclassExpr())
)
}
}
/**
* A call to the ActionView `link_to` helper method.
*
* This generates an HTML anchor tag. The method is not designed to expect
* user-input, so provided paths are not automatically HTML escaped.
*/
class LinkToCall extends ActionViewContextCall {
LinkToCall() { this.getMethodName() = "link_to" }
// TODO: the path can also be specified through other optional arguments
Expr getPathArgument() { result = this.getArgument(1) }
}
// TODO: model flow in/out of template files properly,