Merge pull request #13259 from alexrford/rb/actiondispatch-refactor

Ruby: Refactor and slightly expand `ActionDispatch` modelling
This commit is contained in:
Alex Ford 2023-06-08 11:08:36 +01:00 коммит произвёл GitHub
Родитель d9e3569fe5 9ccfec0571
Коммит 22b9ab43c6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 1183 добавлений и 1145 удалений

Просмотреть файл

@ -204,149 +204,6 @@ private class ActionControllerParamsCall extends ParamsCallImpl {
}
}
/** Modeling for `ActionDispatch::Request`. */
private module Request {
/**
* A call to `request` from within a controller. This is an instance of
* `ActionDispatch::Request`.
*/
private class RequestNode extends DataFlow::CallNode {
RequestNode() { this = actionControllerInstance().getAMethodCall("request") }
}
/**
* A method call on `request`.
*/
private class RequestMethodCall extends DataFlow::CallNode {
RequestMethodCall() {
any(RequestNode r).(DataFlow::LocalSourceNode).flowsTo(this.getReceiver())
}
}
abstract private class RequestInputAccess extends RequestMethodCall,
Http::Server::RequestInputAccess::Range
{
override string getSourceType() { result = "ActionDispatch::Request#" + this.getMethodName() }
}
/**
* A method call on `request` which returns request parameters.
*/
private class ParametersCall extends RequestInputAccess {
ParametersCall() {
this.getMethodName() =
[
"parameters", "params", "GET", "POST", "query_parameters", "request_parameters",
"filtered_parameters"
]
}
override Http::Server::RequestInputKind getKind() {
result = Http::Server::parameterInputKind()
}
}
/** A method call on `request` which returns part or all of the request path. */
private class PathCall extends RequestInputAccess {
PathCall() {
this.getMethodName() =
["path", "filtered_path", "fullpath", "original_fullpath", "original_url", "url"]
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::urlInputKind() }
}
/** A method call on `request` which returns a specific request header. */
private class HeadersCall extends RequestInputAccess {
HeadersCall() {
this.getMethodName() =
[
"authorization", "script_name", "path_info", "user_agent", "referer", "referrer",
"host_authority", "content_type", "host", "hostname", "accept_encoding",
"accept_language", "if_none_match", "if_none_match_etags", "content_mime_type"
]
or
// Request headers are prefixed with `HTTP_` to distinguish them from
// "headers" supplied by Rack middleware.
this.getMethodName() = ["get_header", "fetch_header"] and
this.getArgument(0).getConstantValue().getString().regexpMatch("^HTTP_.+")
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
}
// TODO: each_header
/**
* A method call on `request` which returns part or all of the host.
* This can be influenced by headers such as Host and X-Forwarded-Host.
*/
private class HostCall extends RequestInputAccess {
HostCall() {
this.getMethodName() =
[
"authority", "host", "host_authority", "host_with_port", "hostname", "forwarded_for",
"forwarded_host", "port", "forwarded_port"
]
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
}
/**
* A method call on `request` which is influenced by one or more request
* headers.
*/
private class HeaderTaintedCall extends RequestInputAccess {
HeaderTaintedCall() {
this.getMethodName() = ["media_type", "media_type_params", "content_charset", "base_url"]
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
}
/** A method call on `request` which returns the request body. */
private class BodyCall extends RequestInputAccess {
BodyCall() { this.getMethodName() = ["body", "raw_post", "body_stream"] }
override Http::Server::RequestInputKind getKind() { result = Http::Server::bodyInputKind() }
}
private module Env {
abstract private class Env extends DataFlow::LocalSourceNode { }
/**
* A method call on `request` which returns the rack env.
* This is a hash containing all the information about the request. Values
* under keys starting with `HTTP_` are user-controlled.
*/
private class RequestEnvCall extends DataFlow::CallNode, Env {
RequestEnvCall() { this.getMethodName() = ["env", "filtered_env"] }
}
private import codeql.ruby.frameworks.Rack
private class RackEnv extends Env {
RackEnv() { this = any(Rack::AppCandidate app).getEnv().getALocalUse() }
}
/**
* A read of a user-controlled parameter from the request env.
*/
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
EnvHttpAccess() {
this = any(Env c).getAMethodCall("[]") and
exists(string key | key = this.getArgument(0).getConstantValue().getString() |
key.regexpMatch("^HTTP_.+") or key = "PATH_INFO"
)
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
}
}
}
/** A call to `render` from within a controller. */
private class ActionControllerRenderCall extends RenderCallImpl {
ActionControllerRenderCall() {

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,46 @@
/**
* Models MIME type handling using the `ActionDispatch` library, which is part of Rails.
*/
private import codeql.ruby.Regexp as RE
private import codeql.ruby.frameworks.data.ModelsAsData
/**
* Models MIME type handling using the `ActionDispatch` library, which is part of Rails.
*/
module Mime {
/**
* Type summaries for the `Mime::Type` class, i.e. method calls that produce new
* `Mime::Type` instances.
*/
private class MimeTypeTypeSummary extends ModelInput::TypeModelCsv {
override predicate row(string row) {
// type1;type2;path
row =
[
// Mime[type] : Mime::Type (omitted)
// Method names with brackets like [] cannot be represented in MaD.
// Mime.fetch(type) : Mime::Type
"Mime::Type;Mime!;Method[fetch].ReturnValue",
// Mime::Type.lookup(str) : Mime::Type
"Mime::Type;Mime::Type!;Method[lookup].ReturnValue",
// Mime::Type.lookup_by_extension(str) : Mime::Type
"Mime::Type;Mime::Type!;Method[lookup_by_extension].ReturnValue",
// Mime::Type.register(str) : Mime::Type
"Mime::Type;Mime::Type!;Method[register].ReturnValue",
// Mime::Type.register_alias(str) : Mime::Type
"Mime::Type;Mime::Type!;Method[register_alias].ReturnValue",
]
}
}
/**
* An argument to `Mime::Type#match?`, which is converted to a RegExp via
* `Regexp.new`.
*/
class MimeTypeMatchRegExpInterpretation extends RE::RegExpInterpretation::Range {
MimeTypeMatchRegExpInterpretation() {
this = ModelOutput::getATypeNode("Mime::Type").getAMethodCall(["match?", "=~"]).getArgument(0)
}
}
}

Просмотреть файл

@ -0,0 +1,150 @@
/** Modeling for `ActionDispatch::Request`. */
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.ApiGraphs
private import codeql.ruby.frameworks.ActionController
/** Modeling for `ActionDispatch::Request`. */
module Request {
/**
* A method call against an `ActionDispatch::Request` instance.
*/
private class RequestMethodCall extends DataFlow::CallNode {
RequestMethodCall() {
any(ActionControllerClass cls)
.getSelf()
.getAMethodCall("request")
.(DataFlow::LocalSourceNode)
.flowsTo(this.getReceiver()) or
this =
API::getTopLevelMember("ActionDispatch")
.getMember("Request")
.getInstance()
.getAMethodCall(_)
}
}
abstract private class RequestInputAccess extends RequestMethodCall,
Http::Server::RequestInputAccess::Range
{
override string getSourceType() { result = "ActionDispatch::Request#" + this.getMethodName() }
}
/**
* A method call on `request` which returns request parameters.
*/
private class ParametersCall extends RequestInputAccess {
ParametersCall() {
this.getMethodName() =
[
"parameters", "params", "GET", "POST", "query_parameters", "request_parameters",
"filtered_parameters"
]
}
override Http::Server::RequestInputKind getKind() {
result = Http::Server::parameterInputKind()
}
}
/** A method call on `request` which returns part or all of the request path. */
private class PathCall extends RequestInputAccess {
PathCall() {
this.getMethodName() =
["path", "filtered_path", "fullpath", "original_fullpath", "original_url", "url"]
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::urlInputKind() }
}
/** A method call on `request` which returns a specific request header. */
private class HeadersCall extends RequestInputAccess {
HeadersCall() {
this.getMethodName() =
[
"authorization", "script_name", "path_info", "user_agent", "referer", "referrer",
"host_authority", "content_type", "host", "hostname", "accept_encoding",
"accept_language", "if_none_match", "if_none_match_etags", "content_mime_type"
]
or
// Request headers are prefixed with `HTTP_` to distinguish them from
// "headers" supplied by Rack middleware.
this.getMethodName() = ["get_header", "fetch_header"] and
this.getArgument(0).getConstantValue().getString().regexpMatch("^HTTP_.+")
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
}
// TODO: each_header
/**
* A method call on `request` which returns part or all of the host.
* This can be influenced by headers such as Host and X-Forwarded-Host.
*/
private class HostCall extends RequestInputAccess {
HostCall() {
this.getMethodName() =
[
"authority", "host", "host_authority", "host_with_port", "hostname", "forwarded_for",
"forwarded_host", "port", "forwarded_port"
]
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
}
/**
* A method call on `request` which is influenced by one or more request
* headers.
*/
private class HeaderTaintedCall extends RequestInputAccess {
HeaderTaintedCall() {
this.getMethodName() = ["media_type", "media_type_params", "content_charset", "base_url"]
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
}
/** A method call on `request` which returns the request body. */
private class BodyCall extends RequestInputAccess {
BodyCall() { this.getMethodName() = ["body", "raw_post", "body_stream"] }
override Http::Server::RequestInputKind getKind() { result = Http::Server::bodyInputKind() }
}
private module Env {
abstract private class Env extends DataFlow::LocalSourceNode { }
/**
* A method call on `request` which returns the rack env.
* This is a hash containing all the information about the request. Values
* under keys starting with `HTTP_` are user-controlled.
*/
private class RequestEnvCall extends DataFlow::CallNode, Env {
RequestEnvCall() { this.getMethodName() = ["env", "filtered_env"] }
}
private import codeql.ruby.frameworks.Rack
private class RackEnv extends Env {
RackEnv() { this = any(Rack::AppCandidate app).getEnv().getALocalUse() }
}
/**
* A read of a user-controlled parameter from the request env.
*/
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
EnvHttpAccess() {
this = any(Env c).getAMethodCall("[]") and
exists(string key | key = this.getArgument(0).getConstantValue().getString() |
key.regexpMatch("^HTTP_.+") or key = "PATH_INFO"
)
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
}
}
}

Просмотреть файл

@ -0,0 +1,961 @@
/**
* Models routing configuration specified using the `ActionDispatch` library, which is part of Rails.
*/
private import codeql.ruby.AST
/**
* Models routing configuration specified using the `ActionDispatch` library, which is part of Rails.
*/
module Routing {
/**
* A block that defines some routes.
* Route blocks can contribute to the path or controller namespace of their child routes.
* For example, in the block below
* ```rb
* scope path: "/admin" do
* get "/dashboard", to: "admin_dashboard#show"
* end
* ```
* the route defined by the call to `get` has the full path `/admin/dashboard`.
* We track these contributions via `getPathComponent` and `getControllerComponent`.
*/
abstract private class RouteBlock extends TRouteBlock {
/**
* Gets the name of a primary CodeQL class to which this route block belongs.
*/
string getAPrimaryQlClass() { result = "RouteBlock" }
/**
* Gets a string representation of this route block.
*/
string toString() { none() }
/**
* Gets a `Stmt` within this route block.
*/
abstract Stmt getAStmt();
/**
* Gets the parent of this route block, if one exists.
*/
abstract RouteBlock getParent();
/**
* Gets the `n`th parent of this route block.
* The zeroth parent is this block, the first parent is the direct parent of this block, etc.
*/
RouteBlock getParent(int n) {
if n = 0 then result = this else result = this.getParent().getParent(n - 1)
}
/**
* Gets the component of the path defined by this block, if it exists.
*/
abstract string getPathComponent();
/**
* Gets the component of the controller namespace defined by this block, if it exists.
*/
abstract string getControllerComponent();
/**
* Gets the location of this route block.
*/
abstract Location getLocation();
}
/**
* A route block that is not the top-level block.
* This block will always have a parent.
*/
abstract private class NestedRouteBlock extends RouteBlock {
RouteBlock parent;
override RouteBlock getParent() { result = parent }
override string getAPrimaryQlClass() { result = "NestedRouteBlock" }
}
/**
* A top-level routes block.
* ```rb
* Rails.application.routes.draw do
* ...
* end
* ```
*/
private class TopLevelRouteBlock extends RouteBlock, TTopLevelRouteBlock {
MethodCall call;
// Routing blocks create scopes which define the namespace for controllers and paths,
// though they can be overridden in various ways.
// The namespaces can differ, so we track them separately.
Block block;
TopLevelRouteBlock() { this = TTopLevelRouteBlock(_, call, block) }
override string getAPrimaryQlClass() { result = "TopLevelRouteBlock" }
Block getBlock() { result = block }
override Stmt getAStmt() { result = block.getAStmt() }
override RouteBlock getParent() { none() }
override string toString() { result = call.toString() }
override Location getLocation() { result = call.getLocation() }
override string getPathComponent() { none() }
override string getControllerComponent() { none() }
}
/**
* A route block defined by a call to `constraints`.
* ```rb
* constraints(foo: /some_regex/) do
* get "/posts/:foo", to "posts#something"
* end
* ```
* https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html#method-i-constraints
*/
private class ConstraintsRouteBlock extends NestedRouteBlock, TConstraintsRouteBlock {
private Block block;
private MethodCall call;
ConstraintsRouteBlock() { this = TConstraintsRouteBlock(parent, call, block) }
override string getAPrimaryQlClass() { result = "ConstraintsRouteBlock" }
override Stmt getAStmt() { result = block.getAStmt() }
override string getPathComponent() { result = "" }
override string getControllerComponent() { result = "" }
override string toString() { result = call.toString() }
override Location getLocation() { result = call.getLocation() }
}
/**
* A route block defined by a call to `scope`.
* ```rb
* scope(path: "/some_path", module: "some_module") do
* get "/posts/:foo", to "posts#something"
* end
* ```
* https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html#method-i-scope
*/
private class ScopeRouteBlock extends NestedRouteBlock, TScopeRouteBlock {
private MethodCall call;
private Block block;
ScopeRouteBlock() { this = TScopeRouteBlock(parent, call, block) }
override string getAPrimaryQlClass() { result = "ScopeRouteBlock" }
override Stmt getAStmt() { result = block.getAStmt() }
override string toString() { result = call.toString() }
override Location getLocation() { result = call.getLocation() }
override string getPathComponent() {
call.getKeywordArgument("path").getConstantValue().isStringlikeValue(result)
or
not exists(call.getKeywordArgument("path")) and
call.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getControllerComponent() {
call.getKeywordArgument(["controller", "module"]).getConstantValue().isStringlikeValue(result)
}
}
/**
* A route block defined by a call to `resources`.
* ```rb
* resources :articles do
* get "/comments", to "comments#index"
* end
* ```
* https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resources
*/
private class ResourcesRouteBlock extends NestedRouteBlock, TResourcesRouteBlock {
private MethodCall call;
private Block block;
ResourcesRouteBlock() { this = TResourcesRouteBlock(parent, call, block) }
override string getAPrimaryQlClass() { result = "ResourcesRouteBlock" }
override Stmt getAStmt() { result = block.getAStmt() }
/**
* Gets the `resources` call that gives rise to this route block.
*/
MethodCall getDefiningMethodCall() { result = call }
override string getPathComponent() {
exists(string resource | call.getArgument(0).getConstantValue().isStringlikeValue(resource) |
result = resource + "/:" + singularize(resource) + "_id"
)
}
override string getControllerComponent() { result = "" }
override string toString() { result = call.toString() }
override Location getLocation() { result = call.getLocation() }
}
/**
* A route block that is guarded by a conditional statement.
* For example:
* ```rb
* if Rails.env.test?
* get "/foo/bar", to: "foo#bar"
* end
* ```
* We ignore the condition and analyze both branches to obtain as
* much routing information as possible.
*/
private class ConditionalRouteBlock extends NestedRouteBlock, TConditionalRouteBlock {
private ConditionalExpr e;
ConditionalRouteBlock() { this = TConditionalRouteBlock(parent, e) }
override string getAPrimaryQlClass() { result = "ConditionalRouteBlock" }
override Stmt getAStmt() { result = e.getBranch(_).(StmtSequence).getAStmt() }
override string getPathComponent() { none() }
override string getControllerComponent() { none() }
override string toString() { result = e.toString() }
override Location getLocation() { result = e.getLocation() }
}
/**
* A route block defined by a call to `namespace`.
* ```rb
* namespace :admin do
* resources :posts
* end
* ```
* https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html#method-i-namespace
*/
private class NamespaceRouteBlock extends NestedRouteBlock, TNamespaceRouteBlock {
private MethodCall call;
private Block block;
NamespaceRouteBlock() { this = TNamespaceRouteBlock(parent, call, block) }
override Stmt getAStmt() { result = block.getAStmt() }
override string getPathComponent() { result = this.getNamespace() }
override string getControllerComponent() { result = this.getNamespace() }
private string getNamespace() {
call.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string toString() { result = call.toString() }
override Location getLocation() { result = call.getLocation() }
}
/**
* A route configuration. This defines a combination of HTTP method and URL
* path which should be routed to a particular controller-action pair.
* This can arise from an explicit call to a routing method, for example:
* ```rb
* get "/photos", to: "photos#index"
* ```
* or via a convenience method like `resources`, which defines multiple routes at once:
* ```rb
* resources :photos
* ```
*/
class Route extends TRoute instanceof RouteImpl {
/**
* Gets the name of a primary CodeQL class to which this route belongs.
*/
string getAPrimaryQlClass() { result = "Route" }
/** Gets a string representation of this route. */
string toString() { result = super.toString() }
/**
* Gets the location of the method call that defines this route.
*/
Location getLocation() { result = super.getLocation() }
/**
* Gets the full controller targeted by this route.
*/
string getController() { result = super.getController() }
/**
* Gets the action targeted by this route.
*/
string getAction() { result = super.getAction() }
/**
* Gets the HTTP method of this route.
* The result is one of [get, post, put, patch, delete].
*/
string getHttpMethod() { result = super.getHttpMethod() }
/**
* Gets the full path of the route.
*/
string getPath() { result = super.getPath() }
/**
* Get a URL capture. This is a wildcard URL segment whose value is placed in `params`.
* For example, in
* ```ruby
* get "/foo/:bar/baz", to: "users#index"
* ```
* the capture is `:bar`.
*/
string getACapture() { result = super.getACapture() }
}
/**
* The implementation of `Route`.
* This class is abstract and is thus kept private so we don't expose it to
* users.
* Extend this class to add new instances of routes.
*/
abstract private class RouteImpl extends TRoute {
/**
* Gets the name of a primary CodeQL class to which this route belongs.
*/
string getAPrimaryQlClass() { result = "RouteImpl" }
MethodCall method;
/** Gets a string representation of this route. */
string toString() { result = method.toString() }
/**
* Gets the location of the method call that defines this route.
*/
Location getLocation() { result = method.getLocation() }
/**
* Gets the method call that defines this route.
*/
MethodCall getDefiningMethodCall() { result = method }
/**
* Get the last component of the path. For example, in
* ```rb
* get "/photos", to: "photos#index"
* ```
* this is `/photos`.
* If the string has any interpolations, this predicate will have no result.
*/
abstract string getLastPathComponent();
/**
* Gets the HTTP method of this route.
* The result is one of [get, post, put, patch, delete].
*/
abstract string getHttpMethod();
/**
* Gets the last controller component.
* This is the controller specified in the route itself.
*/
abstract string getLastControllerComponent();
/**
* Gets a component of the controller.
* This behaves identically to `getPathComponent`, but for controller information.
*/
string getControllerComponent(int n) {
if n = 0
then result = this.getLastControllerComponent()
else result = this.getParentBlock().getParent(n - 1).getControllerComponent()
}
/**
* Gets the full controller targeted by this route.
*/
string getController() {
result =
concat(int n |
this.getControllerComponent(n) != ""
|
this.getControllerComponent(n), "/" order by n desc
)
}
/**
* Gets the action targeted by this route.
*/
abstract string getAction();
/**
* Gets the parent `RouteBlock` of this route.
*/
abstract RouteBlock getParentBlock();
/**
* Gets a component of the path. Components are numbered from 0 up, where 0
* is the last component, 1 is the second-last, etc.
* For example, in the following route:
*
* ```rb
* namespace path: "foo" do
* namespace path: "bar" do
* get "baz", to: "foo#bar
* end
* end
* ```
*
* the components are:
*
* | n | component
* |---|----------
* | 0 | baz
* | 1 | bar
* | 2 | foo
*/
string getPathComponent(int n) {
if n = 0
then result = this.getLastPathComponent()
else result = this.getParentBlock().getParent(n - 1).getPathComponent()
}
/**
* Gets the full path of the route.
*/
string getPath() {
result =
concat(int n |
this.getPathComponent(n) != ""
|
// Strip leading and trailing slashes from each path component before combining
stripSlashes(this.getPathComponent(n)), "/" order by n desc
)
}
/**
* Get a URL capture. This is a wildcard URL segment whose value is placed in `params`.
* For example, in
* ```ruby
* get "/foo/:bar/baz", to: "users#index"
* ```
* the capture is `:bar`.
* We don't currently make use of this, but it may be useful in future to more accurately
* model the contents of the `params` hash.
*/
string getACapture() { result = this.getPathComponent(_).regexpFind(":[^:/]+", _, _) }
}
/**
* A route generated by an explicit call to `get`, `post`, etc.
*
* ```ruby
* get "/photos", to: "photos#index"
* put "/photos/:id", to: "photos#update"
* ```
*/
private class ExplicitRoute extends RouteImpl, TExplicitRoute {
RouteBlock parentBlock;
ExplicitRoute() { this = TExplicitRoute(parentBlock, method) }
override string getAPrimaryQlClass() { result = "ExplicitRoute" }
override RouteBlock getParentBlock() { result = parentBlock }
override string getLastPathComponent() {
method.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getLastControllerComponent() {
method.getKeywordArgument("controller").getConstantValue().isStringlikeValue(result)
or
not exists(method.getKeywordArgument("controller")) and
(
result = extractController(this.getActionString())
or
// If controller is not specified, and we're in a `resources` route block, use the controller of that route.
// For example, in
//
// resources :posts do
// get "timestamp", to: :timestamp
// end
//
// The route is GET /posts/:post_id/timestamp => posts/timestamp
not exists(extractController(this.getActionString())) and
exists(ResourcesRoute r |
r.getDefiningMethodCall() = parentBlock.(ResourcesRouteBlock).getDefiningMethodCall()
|
result = r.getLastControllerComponent()
)
)
}
private string getActionString() {
method.getKeywordArgument("to").getConstantValue().isStringlikeValue(result)
or
method.getKeywordArgument("to").(MethodCall).getMethodName() = "redirect" and
result = "<redirect>#<redirect>"
}
override string getAction() {
// get "/photos", action: "index"
method.getKeywordArgument("action").getConstantValue().isStringlikeValue(result)
or
not exists(method.getKeywordArgument("action")) and
(
// get "/photos", to: "photos#index"
// get "/photos", to: redirect("some_url")
result = extractAction(this.getActionString())
or
// resources :photos, only: [] do
// get "/", to: "index"
// end
not exists(extractAction(this.getActionString())) and result = this.getActionString()
or
// get :some_action
not exists(this.getActionString()) and
method.getArgument(0).getConstantValue().isStringlikeValue(result)
)
}
override string getHttpMethod() { result = method.getMethodName().toString() }
}
/**
* A route generated by a call to `resources`.
*
* ```ruby
* resources :photos
* ```
* This creates eight routes, equivalent to the following code:
* ```ruby
* get "/photos", to: "photos#index"
* get "/photos/new", to: "photos#new"
* post "/photos", to: "photos#create"
* get "/photos/:id", to: "photos#show"
* get "/photos/:id/edit", to: "photos#edit"
* patch "/photos/:id", to: "photos#update"
* put "/photos/:id", to: "photos#update"
* delete "/photos/:id", to: "photos#delete"
* ```
*
* `resources` can take a block. Any routes defined inside the block will inherit a path component of
* `/<resource>/:<resource>_id`. For example:
*
* ```ruby
* resources :photos do
* get "/foo", to: "photos#foo"
* end
* ```
* This creates the eight default routes, plus one more, which is nested under "/photos/:photo_id", equivalent to:
* ```ruby
* get "/photos/:photo_id/foo", to: "photos#foo"
* ```
*/
private class ResourcesRoute extends RouteImpl, TResourcesRoute {
RouteBlock parent;
string action;
string httpMethod;
string pathComponent;
ResourcesRoute() {
exists(string resource |
this = TResourcesRoute(parent, method, action) and
method.getArgument(0).getConstantValue().isStringlikeValue(resource) and
isDefaultResourceRoute(resource, httpMethod, pathComponent, action)
)
}
override string getAPrimaryQlClass() { result = "ResourcesRoute" }
override RouteBlock getParentBlock() { result = parent }
override string getLastPathComponent() { result = pathComponent }
override string getLastControllerComponent() {
method.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getAction() { result = action }
override string getHttpMethod() { result = httpMethod }
}
/**
* A route generated by a call to `resource`.
* This is like a `resources` route, but creates routes for a singular resource.
* This means there's no index route, no id parameter, and the resource name is expected to be singular.
* It will still be routed to a pluralised controller name.
* ```ruby
* resource :account
* ```
*/
private class SingularResourceRoute extends RouteImpl, TResourceRoute {
RouteBlock parent;
string action;
string httpMethod;
string pathComponent;
SingularResourceRoute() {
exists(string resource |
this = TResourceRoute(parent, method, action) and
method.getArgument(0).getConstantValue().isStringlikeValue(resource) and
isDefaultSingularResourceRoute(resource, httpMethod, pathComponent, action)
)
}
override string getAPrimaryQlClass() { result = "SingularResourceRoute" }
override RouteBlock getParentBlock() { result = parent }
override string getLastPathComponent() { result = pathComponent }
override string getLastControllerComponent() {
method.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getAction() { result = action }
override string getHttpMethod() { result = httpMethod }
}
/**
* A route generated by a call to `match`.
* This is a lower level primitive that powers `get`, `post` etc.
* The first argument can be a path or a (path, controller-action) pair.
* The controller, action and HTTP method can be specified with the
* `controller:`, `action:` and `via:` keyword arguments, respectively.
* ```ruby
* match 'photos/:id' => 'photos#show', via: :get
* match 'photos/:id', to: 'photos#show', via: :get
* match 'photos/:id', to 'photos#show', via: [:get, :post]
* match 'photos/:id', controller: 'photos', action: 'show', via: :get
* ```
*/
private class MatchRoute extends RouteImpl, TMatchRoute {
private RouteBlock parent;
MatchRoute() { this = TMatchRoute(parent, method) }
override string getAPrimaryQlClass() { result = "MatchRoute" }
override RouteBlock getParentBlock() { result = parent }
override string getLastPathComponent() {
[method.getArgument(0), method.getArgument(0).(Pair).getKey()]
.getConstantValue()
.isStringlikeValue(result)
}
override string getLastControllerComponent() {
result =
extractController(method.getKeywordArgument("to").getConstantValue().getStringlikeValue()) or
method.getKeywordArgument("controller").getConstantValue().isStringlikeValue(result) or
result =
extractController(method
.getArgument(0)
.(Pair)
.getValue()
.getConstantValue()
.getStringlikeValue())
}
override string getHttpMethod() {
exists(string via |
method.getKeywordArgument("via").getConstantValue().isStringlikeValue(via)
|
via = "all" and result = anyHttpMethod()
or
via != "all" and result = via
)
or
result =
method
.getKeywordArgument("via")
.(ArrayLiteral)
.getElement(_)
.getConstantValue()
.getStringlikeValue()
}
override string getAction() {
result =
extractAction(method.getKeywordArgument("to").getConstantValue().getStringlikeValue()) or
method.getKeywordArgument("action").getConstantValue().isStringlikeValue(result) or
result =
extractAction(method
.getArgument(0)
.(Pair)
.getValue()
.getConstantValue()
.getStringlikeValue())
}
}
private import Cached
/**
* This module contains the IPA types backing `RouteBlock` and `Route`, cached for performance.
*/
cached
private module Cached {
cached
newtype TRouteBlock =
TTopLevelRouteBlock(MethodCall routes, MethodCall draw, Block b) {
routes.getMethodName() = "routes" and
draw.getMethodName() = "draw" and
draw.getReceiver() = routes and
draw.getBlock() = b
} or
// constraints(foo: /some_regex/) do
// get "/posts/:foo", to "posts#something"
// end
TConstraintsRouteBlock(RouteBlock parent, MethodCall constraints, Block b) {
parent.getAStmt() = constraints and
constraints.getMethodName() = "constraints" and
constraints.getBlock() = b
} or
// scope(path: "/some_path", module: "some_module") do
// get "/posts/:foo", to "posts#something"
// end
TScopeRouteBlock(RouteBlock parent, MethodCall scope, Block b) {
parent.getAStmt() = scope and scope.getMethodName() = "scope" and scope.getBlock() = b
} or
// resources :articles do
// get "/comments", to "comments#index"
// end
TResourcesRouteBlock(RouteBlock parent, MethodCall resources, Block b) {
parent.getAStmt() = resources and
resources.getMethodName() = "resources" and
resources.getBlock() = b
} or
// A conditional statement guarding some routes.
// We ignore the condition and analyze both branches to obtain as
// much routing information as possible.
TConditionalRouteBlock(RouteBlock parent, ConditionalExpr e) { parent.getAStmt() = e } or
// namespace :admin do
// resources :posts
// end
TNamespaceRouteBlock(RouteBlock parent, MethodCall namespace, Block b) {
parent.getAStmt() = namespace and
namespace.getMethodName() = "namespace" and
namespace.getBlock() = b
}
/**
* A route configuration. See `Route` for more info
*/
cached
newtype TRoute =
/**
* See `ExplicitRoute`
*/
TExplicitRoute(RouteBlock b, MethodCall m) {
b.getAStmt() = m and m.getMethodName() = anyHttpMethod()
} or
/**
* See `ResourcesRoute`
*/
TResourcesRoute(RouteBlock b, MethodCall m, string action) {
b.getAStmt() = m and
m.getMethodName() = "resources" and
action in ["show", "index", "new", "edit", "create", "update", "destroy"] and
applyActionFilters(m, action)
} or
/**
* See `SingularResourceRoute`
*/
TResourceRoute(RouteBlock b, MethodCall m, string action) {
b.getAStmt() = m and
m.getMethodName() = "resource" and
action in ["show", "new", "edit", "create", "update", "destroy"] and
applyActionFilters(m, action)
} or
/**
* See `MatchRoute`
*/
TMatchRoute(RouteBlock b, MethodCall m) { b.getAStmt() = m and m.getMethodName() = "match" }
}
/**
* Several routing methods support the keyword arguments `only:` and `except:`.
* - `only:` restricts the set of actions to just those in the argument.
* - `except:` removes the given actions from the set.
*/
bindingset[action]
private predicate applyActionFilters(MethodCall m, string action) {
// Respect the `only` keyword argument, which restricts the set of actions.
(
not exists(m.getKeywordArgument("only"))
or
exists(Expr only | only = m.getKeywordArgument("only") |
[only.(ArrayLiteral).getElement(_), only].getConstantValue().isStringlikeValue(action)
)
) and
// Respect the `except` keyword argument, which removes actions from the default set.
(
not exists(m.getKeywordArgument("except"))
or
exists(Expr except | except = m.getKeywordArgument("except") |
[except.(ArrayLiteral).getElement(_), except].getConstantValue().getStringlikeValue() !=
action
)
)
}
/**
* Holds if the (resource, method, path, action) combination would be generated by a call to `resources :<resource>`.
*/
bindingset[resource]
private predicate isDefaultResourceRoute(
string resource, string method, string path, string action
) {
action = "create" and
(method = "post" and path = "/" + resource)
or
action = "index" and
(method = "get" and path = "/" + resource)
or
action = "new" and
(method = "get" and path = "/" + resource + "/new")
or
action = "edit" and
(method = "get" and path = "/" + resource + ":id/edit")
or
action = "show" and
(method = "get" and path = "/" + resource + "/:id")
or
action = "update" and
(method in ["put", "patch"] and path = "/" + resource + "/:id")
or
action = "destroy" and
(method = "delete" and path = "/" + resource + "/:id")
}
/**
* Holds if the (resource, method, path, action) combination would be generated by a call to `resource :<resource>`.
*/
bindingset[resource]
private predicate isDefaultSingularResourceRoute(
string resource, string method, string path, string action
) {
action = "create" and
(method = "post" and path = "/" + resource)
or
action = "new" and
(method = "get" and path = "/" + resource + "/new")
or
action = "edit" and
(method = "get" and path = "/" + resource + "/edit")
or
action = "show" and
(method = "get" and path = "/" + resource)
or
action = "update" and
(method in ["put", "patch"] and path = "/" + resource)
or
action = "destroy" and
(method = "delete" and path = "/" + resource)
}
/**
* Extract the controller from a Rails routing string
* ```
* extractController("posts#show") = "posts"
* ```
*/
bindingset[input]
private string extractController(string input) { result = input.regexpCapture("([^#]+)#.+", 1) }
/**
* Extract the action from a Rails routing string
* ```
* extractAction("posts#show") = "show"
*/
bindingset[input]
private string extractAction(string input) { result = input.regexpCapture("[^#]+#(.+)", 1) }
/**
* Returns the lowercase name of every HTTP method we support.
*/
private string anyHttpMethod() { result = ["get", "post", "put", "patch", "delete"] }
/**
* The inverse of `pluralize`. If `input` is a plural word, it returns the
* singular version.
*
* Examples:
*
* - photos -> photo
* - stories -> story
* - not_plural -> not_plural
*/
bindingset[input]
private string singularize(string input) {
exists(string prefix | prefix = input.regexpCapture("(.*)ies", 1) | result = prefix + "y")
or
not input.matches("%ies") and
exists(string prefix | prefix = input.regexpCapture("(.*)s", 1) | result = prefix)
or
not input.regexpMatch(".*(ies|s)") and result = input
}
/**
* Convert a camel-case string to underscore case. Converts `::` to `/`.
* This can be used to convert ActiveRecord controller names to a canonical form that matches the routes they handle.
* Note: All-uppercase words like `CONSTANT` are not handled correctly.
*/
bindingset[base]
string underscore(string base) {
base = "" and result = ""
or
result =
base.charAt(0).toLowerCase() +
// Walk along the string, keeping track of the previous character
// in order to determine if we've crossed a boundary.
// Boundaries are:
// - lower case to upper case: B in FooBar
// - entering a namespace: B in Foo::Bar
concat(int i, string prev, string char |
prev = base.charAt(i) and
char = base.charAt(i + 1)
|
any(string s |
char.regexpMatch("[A-Za-z0-9]") and
if prev = ":"
then s = "/" + char.toLowerCase()
else
if prev.isLowercase() and char.isUppercase()
then s = "_" + char.toLowerCase()
else s = char.toLowerCase()
)
order by
i
)
}
/**
* Strip leading and trailing forward slashes from the string.
*/
bindingset[input]
private string stripSlashes(string input) {
result = input.regexpReplaceAll("^/+(.+)$", "$1").regexpReplaceAll("^(.*[^/])/+$", "$1")
}
}

Просмотреть файл

@ -36,8 +36,8 @@ actionDispatchRoutes
actionDispatchControllerMethods
| app/config/routes.rb:2:3:8:5 | call to resources | app/controllers/posts_controller.rb:2:3:3:5 | index |
| app/config/routes.rb:2:3:8:5 | call to resources | app/controllers/posts_controller.rb:5:3:6:5 | show |
| app/config/routes.rb:3:5:6:7 | call to resources | app/controllers/comments_controller.rb:2:3:36:5 | index |
| app/config/routes.rb:3:5:6:7 | call to resources | app/controllers/comments_controller.rb:38:3:39:5 | show |
| app/config/routes.rb:3:5:6:7 | call to resources | app/controllers/comments_controller.rb:2:3:39:5 | index |
| app/config/routes.rb:3:5:6:7 | call to resources | app/controllers/comments_controller.rb:41:3:42:5 | show |
| app/config/routes.rb:7:5:7:37 | call to post | app/controllers/posts_controller.rb:8:3:9:5 | upvote |
| app/config/routes.rb:27:3:27:48 | call to match | app/controllers/photos_controller.rb:2:3:3:5 | show |
| app/config/routes.rb:28:3:28:50 | call to match | app/controllers/photos_controller.rb:2:3:3:5 | show |
@ -66,3 +66,17 @@ mimeTypeMatchRegExpInterpretations
| mime_type.rb:12:7:12:15 | "foo/bar" |
| mime_type.rb:13:11:13:11 | s |
| mime_type.rb:14:7:14:7 | s |
requestInputAccesses
| app/controllers/comments_controller.rb:3:5:3:18 | call to params |
| app/controllers/comments_controller.rb:4:5:4:22 | call to parameters |
| app/controllers/comments_controller.rb:5:5:5:15 | call to GET |
| app/controllers/comments_controller.rb:6:5:6:16 | call to POST |
| app/controllers/comments_controller.rb:7:5:7:28 | call to query_parameters |
| app/controllers/comments_controller.rb:8:5:8:30 | call to request_parameters |
| app/controllers/comments_controller.rb:9:5:9:31 | call to filtered_parameters |
| app/controllers/comments_controller.rb:38:5:38:14 | call to params |
| app/controllers/foo/bars_controller.rb:10:27:10:33 | call to cookies |
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |
| app/controllers/foo/bars_controller.rb:22:10:22:15 | call to params |

Просмотреть файл

@ -2,6 +2,7 @@ private import codeql.ruby.AST
private import codeql.ruby.frameworks.ActionDispatch
private import codeql.ruby.frameworks.ActionController
private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts
private import codeql.ruby.frameworks.data.ModelsAsData
private import codeql.ruby.DataFlow
private import codeql.ruby.Regexp as RE
@ -36,3 +37,5 @@ query predicate mimeTypeMatchRegExpInterpretations(
) {
any()
}
query predicate requestInputAccesses(Http::Server::RequestInputAccess a) { any() }

Просмотреть файл

@ -33,6 +33,9 @@ class CommentsController < ApplicationController
response.last_modified = Date.yesterday
response.weak_etag = "value"
response.strong_etag = "value"
req = ActionDispatch::Request.new(request.env)
req.params
end
def show