Ruby: Recognise Rails render calls as HTTP responses

This commit is contained in:
Harry Maclean
2022-08-16 13:40:06 +12:00
parent 79bae0caeb
commit 7ef6ffbc54
7 changed files with 92 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Calls to `render` in Rails controllers and views are now recognised as HTTP
response bodies.

View File

@@ -149,26 +149,26 @@ class CookiesSource extends HTTP::Server::RequestInputAccess::Range {
override string getSourceType() { result = "ActionController::Metal#cookies" }
}
// A call to `cookies` from within a controller.
/** A call to `cookies` from within a controller. */
private class ActionControllerCookiesCall extends ActionControllerContextCall, CookiesCall { }
// A call to `params` from within a controller.
/** A call to `params` from within a controller. */
private class ActionControllerParamsCall extends ActionControllerContextCall, ParamsCall { }
// A call to `render` from within a controller.
/** A call to `render` from within a controller. */
private class ActionControllerRenderCall extends ActionControllerContextCall, RenderCall { }
// A call to `render_to` from within a controller.
/** A call to `render_to` from within a controller. */
private class ActionControllerRenderToCall extends ActionControllerContextCall, RenderToCall { }
// A call to `html_safe` from within a controller.
/** A call to `html_safe` from within a controller. */
private class ActionControllerHtmlSafeCall extends HtmlSafeCall {
ActionControllerHtmlSafeCall() {
this.getEnclosingModule() instanceof ActionControllerControllerClass
}
}
// A call to `html_escape` from within a controller.
/** A call to `html_escape` from within a controller. */
private class ActionControllerHtmlEscapeCall extends HtmlEscapeCall {
ActionControllerHtmlEscapeCall() {
this.getEnclosingModule() instanceof ActionControllerControllerClass

View File

@@ -123,7 +123,55 @@ abstract class RenderCall extends MethodCall {
// TODO: implicit renders in controller actions
}
// A call to the `render` method within the context of a template.
/**
* A call to `render`, `render_to_body` or `render_to_string`, seen as an
* `HttpResponse`.
*/
private class RenderCallAsHttpResponse extends DataFlow::CallNode, HTTP::Server::HttpResponse::Range {
RenderCallAsHttpResponse() {
this.asExpr().getExpr() instanceof RenderCall or
this.asExpr().getExpr() instanceof RenderToCall
}
// `render` is a very polymorphic method - all of these are valid calls:
// render @user
// render "path/to/template"
// render html: "<html></html>"
// render json: { "some" => "hash" }
// render body: "some text"
override DataFlow::Node getBody() {
// A positional argument, e.g.
// render @user
// render "path/to/template"
result = this.getArgument(_) and
not result.asExpr() instanceof ExprNodes::PairCfgNode
or
result = this.getKeywordArgument(["html", "json", "body", "inline", "plain", "js", "file"])
}
override DataFlow::Node getMimetypeOrContentTypeArg() {
result = this.getKeywordArgument("content_type")
}
override string getMimetype() {
exists(this.getKeywordArgument("json")) and result = "application/json"
or
exists(this.getKeywordArgument("plain")) and result = "text/plain"
or
exists(this.getKeywordArgument("html")) and result = "text/html"
or
exists(this.getKeywordArgument("xml")) and result = "application/xml"
or
exists(this.getKeywordArgument("js")) and result = "text/javascript"
or
not exists(this.getKeywordArgument(["json", "plain", "html", "xml", "js"])) and
result = super.getMimetype()
}
override string getMimetypeDefault() { result = "text/html" }
}
/** A call to the `render` method within the context of a template. */
private class ActionViewRenderCall extends RenderCall, ActionViewContextCall { }
/**

View File

@@ -4,7 +4,7 @@ actionControllerControllerClasses
| active_record/ActiveRecord.rb:66:1:94:3 | BazController |
| active_record/ActiveRecord.rb:96:1:104:3 | AnnotatedController |
| app/controllers/comments_controller.rb:1:1:7:3 | CommentsController |
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController |
| app/controllers/foo/bars_controller.rb:3:1:46:3 | BarsController |
| app/controllers/photos_controller.rb:1:1:4:3 | PhotosController |
| app/controllers/posts_controller.rb:1:1:10:3 | PostsController |
| app/controllers/users/notifications_controller.rb:2:3:5:5 | NotificationsController |
@@ -28,6 +28,7 @@ actionControllerActionMethods
| app/controllers/foo/bars_controller.rb:20:3:24:5 | show |
| app/controllers/foo/bars_controller.rb:26:3:28:5 | go_back |
| app/controllers/foo/bars_controller.rb:30:3:32:5 | go_back_2 |
| app/controllers/foo/bars_controller.rb:34:3:39:5 | show_2 |
| app/controllers/photos_controller.rb:2:3:3:5 | show |
| app/controllers/posts_controller.rb:2:3:3:5 | index |
| app/controllers/posts_controller.rb:5:3:6:5 | show |
@@ -103,8 +104,8 @@ redirectToCalls
| app/controllers/foo/bars_controller.rb:31:5:31:56 | call to redirect_back |
actionControllerHelperMethods
getAssociatedControllerClasses
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
| app/controllers/foo/bars_controller.rb:3:1:46:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
| app/controllers/foo/bars_controller.rb:3:1:46:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
controllerTemplateFiles
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
| app/controllers/foo/bars_controller.rb:3:1:46:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
| app/controllers/foo/bars_controller.rb:3:1:46:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |

View File

@@ -14,9 +14,19 @@ rawCalls
renderCalls
| app/controllers/foo/bars_controller.rb:6:5:6:37 | call to render |
| app/controllers/foo/bars_controller.rb:23:5:23:76 | call to render |
| app/controllers/foo/bars_controller.rb:37:5:37:17 | call to render |
| app/controllers/foo/bars_controller.rb:35:5:35:33 | call to render |
| app/controllers/foo/bars_controller.rb:38:5:38:50 | call to render |
| app/controllers/foo/bars_controller.rb:44:5:44:17 | call to render |
| app/views/foo/bars/show.html.erb:31:5:31:89 | call to render |
renderToCalls
| app/controllers/foo/bars_controller.rb:15:16:15:97 | call to render_to_string |
| app/controllers/foo/bars_controller.rb:36:12:36:67 | call to render_to_string |
linkToCalls
| app/views/foo/bars/show.html.erb:33:5:33:41 | call to link_to |
httpResponses
| app/controllers/foo/bars_controller.rb:15:16:15:97 | call to render_to_string | app/controllers/foo/bars_controller.rb:15:33:15:47 | "foo/bars/show" | text/html |
| app/controllers/foo/bars_controller.rb:23:5:23:76 | call to render | app/controllers/foo/bars_controller.rb:23:12:23:26 | "foo/bars/show" | text/html |
| app/controllers/foo/bars_controller.rb:35:5:35:33 | call to render | app/controllers/foo/bars_controller.rb:35:18:35:33 | call to [] | application/json |
| app/controllers/foo/bars_controller.rb:36:12:36:67 | call to render_to_string | app/controllers/foo/bars_controller.rb:36:29:36:33 | @user | application/json |
| app/controllers/foo/bars_controller.rb:38:5:38:50 | call to render | app/controllers/foo/bars_controller.rb:38:12:38:22 | call to backtrace | text/plain |
| app/controllers/foo/bars_controller.rb:44:5:44:17 | call to render | app/controllers/foo/bars_controller.rb:44:12:44:17 | "show" | text/html |

View File

@@ -1,5 +1,7 @@
import codeql.ruby.frameworks.ActionController
import codeql.ruby.frameworks.ActionView
private import codeql.ruby.frameworks.ActionController
private import codeql.ruby.frameworks.ActionView
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
query predicate htmlSafeCalls(HtmlSafeCall c) { any() }
@@ -10,3 +12,7 @@ query predicate renderCalls(RenderCall c) { any() }
query predicate renderToCalls(RenderToCall c) { any() }
query predicate linkToCalls(LinkToCall c) { any() }
query predicate httpResponses(HTTP::Server::HttpResponse r, DataFlow::Node body, string mimeType) {
r.getBody() = body and r.getMimetype() = mimeType
}

View File

@@ -31,6 +31,13 @@ class BarsController < ApplicationController
redirect_back fallback_location: { action: "index" }
end
def show_2
render json: { some: "data" }
body = render_to_string @user, content_type: "application/json"
rescue => e
render e.backtrace, content_type: "text/plain"
end
private
def unreachable_action