The Wayback Machine - http://web.archive.org/web/20260107104812/https://github.com/github/codeql/pull/7061
Skip to content

Conversation

@hmac
Copy link
Contributor

@hmac hmac commented Nov 4, 2021

Add Route classes which model Rails routing information, typically
defined in a routes.rb file. We extract only the most basic
information: HTTP method, path, controller and action. This is enough to
determine whether a given controller method is a route handler, and what
HTTP method it handles, which is useful for, among other things, the URL
redirect query.

This is quite a big PR with some probably-questionable design decisions, but does work correctly at least. I'd appreciate feedback on a) whether this is worth it and b) if there's a
more concise/more performant way to do it.

@hmac hmac added the Ruby label Nov 4, 2021
@hmac hmac force-pushed the hmac/actiondispatch branch 3 times, most recently from 4e858e9 to 01b51c4 Compare November 10, 2021 08:55
@hmac hmac force-pushed the hmac/actiondispatch branch from 01b51c4 to 2fe011f Compare November 19, 2021 15:25
@hmac hmac force-pushed the hmac/actiondispatch branch from 9820a8e to 0fdbe22 Compare December 13, 2021 02:53
@hmac hmac marked this pull request as ready for review December 13, 2021 04:02
@hmac hmac requested a review from a team as a code owner December 13, 2021 04:02
Copy link
Contributor

@nickrolfe nickrolfe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it was a ton of work. I've only got part-way through reviewing this, but it looks great!


override string getAction() { result = action }

override string getHTTPMethod() { result = httpMethod }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this returning patch for a call like this:

resources :foo,
    path: "/foo/:foo_id/bar",
    only: %w(new create edit update destroy),
    ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, am I mixing up actions and HTTP methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PATCH is a valid, if rarely used, HTTP method which acts similarly to PUT, but is intended for partial updates to a resource (whereas PUT is for completely replacing the resource). Rails prefers PATCH for updates because it is a better fit semantically. (https://guides.rubyonrails.org/v6.0/4_0_release_notes.html)

@hmac hmac force-pushed the hmac/actiondispatch branch 2 times, most recently from 2989cab to 5e9b250 Compare December 15, 2021 22:00
@nickrolfe
Copy link
Contributor

The tests are failing because of a warning about the decapitalize predicate being unused. You also have a number of alerts from QL for QL. It'd be good to fix those.

Otherwise, LGTM.

@hmac hmac force-pushed the hmac/actiondispatch branch from 183a1bb to aa96717 Compare December 21, 2021 19:18
@hmac hmac force-pushed the hmac/actiondispatch branch from aa96717 to 0fd3e46 Compare January 5, 2022 23:57
Copy link
Contributor

@alexrford alexrford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice - very clearly structured and well commented. Only a handful of minor comments.

result = method.getKeywordArgument("to").(StringlikeLiteral).getValueText()
or
method.getKeywordArgument("to").(MethodCall).getMethodName() = "redirect" and
result = "<redirect>#<redirect>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a placeholder value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took me a while to remember what this is for! It covers cases like

get "/stories" => redirect("/posts")

In this case there's no controller/action combo that we can map GET /stories to, except by trying to resolve /posts to a route and going from there. That seemed like a lot of effort so instead we just return <redirect>#<redirect> for the action string.

Thinking about it again, perhaps we should just return no result in this case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably okay as it is - by doing this it looks like getAction() will return "<redirect>", so that's a bit more informative than having no result.


override string getHTTPMethod() {
result = method.getKeywordArgument("via").(StringlikeLiteral).getValueText() or
result = method.getKeywordArgument("via").(ArrayLiteral).getElement(_).getValueText()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if this should have a special case for :all where the result = ["get", "post", "put", "patch", "delete"] (https://guides.rubyonrails.org/routing.html#http-verb-constraints)?

@hmac hmac force-pushed the hmac/actiondispatch branch from 0fd3e46 to 7369846 Compare January 26, 2022 02:59
Copy link
Contributor

@alexrford alexrford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hmac just one last comment, let me know when it's done and I'll approve this PR.

|
via = "all" and result = anyHttpMethod()
or
via != "all" and result = "via"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
via != "all" and result = "via"
via != "all" and result = via

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 good catch!

Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few drive-by comments.

ActionDispatch::Route getARoute() {
result.getController() + "_controller" =
ActionDispatch::underscore(namespaceDeclaration(controllerClass)) and
this.getName() = result.getAction()
Copy link
Contributor

@hvitved hvitved Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a candidate for a bad join, but I don't know if joining on getName() and getAction() alone can yield a large set in practice. It should be possible to join simultaneously on name and controller, see e.g. #7738 for an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still getting my head around this problem and its mitigation. I've replaced this line with a call to isActionControllerMethod(this, result.getAction(), controllerClass) (749dc09), which will hopefully force a join simultaneously on getName(), getAction() and controllerClass. Is that enough to protect against this issue or do we need to do something else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add a new predicate

pragma[nomagic]
private predicate isRoute(
  ActionDispatch::Route r, string name, ActionControllerControllerClass controllerClass
) {
  r.getController() + "_controller" =
    ActionDispatch::underscore(namespaceDeclaration(controllerClass)) and
  name = r.getAction()
}

and change the body to

    exists(string name |
      isRoute(result, name, controllerClass) and
      isActionControllerMethod(this, name, controllerClass)
    )

then simultaneous joining on name and controllerClass should be ensured.

hmac and others added 10 commits February 2, 2022 16:26
Add `Route` classes which model Rails routing information, typically
defined in a `routes.rb` file. We extract only the most basic
information: HTTP method, path, controller and action. This is enough to
determine whether a given controller method is a route handler, and what
HTTP method it handles, which is useful for, among other things, the URL
redirect query.
Handlers for non-GET requests aren't vulnerable to URL redirect attacks,
because browsers won't initiate non-GET requests when you click a link.

We can use Rails routing information, if present, to filter out any
handlers for non-GET requests.
This predicate isn't used.
This shows how the predicate behaves, as well as a case where it goes
wrong.
Any classes/predicates not used externally or in tests are now private.
Also fix some typos.
This version is much shorter and hopefully performs a bit better.
hmac added 11 commits February 2, 2022 16:26
ActionDispatch modelling now understands that

    match "/foo", to: "foo#bar", via: :all

is equivalent to

    match "/foo",
      to: "foo#bar",
      via: [:get, :post, :put, :patch, :delete]
`x.isStringOrSymbol(result)` is slightly terser than
`result = x.getStringOrSymbol()`.
By joining simultaneously on controller class and name.
Make ActionDispatch::Route into a private class
ActionDispatch::RouteImpl, defining a new class Route which exposes the
necessary public API from RouteImpl.

Also rename getHTTPMethod to getHttpMethod.
@hmac hmac force-pushed the hmac/actiondispatch branch from c94f4c1 to 80835a5 Compare February 2, 2022 21:43
}
}

private predicate isRoute(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding pragma[nomagic] to ensure that the compiler does not mess up our optimization (e.g. by inlining the predicate)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@hmac hmac dismissed alexrford’s stale review February 9, 2022 20:46

Fixed the issue with via

@hmac hmac merged commit f302222 into main Feb 9, 2022
@hmac hmac deleted the hmac/actiondispatch branch February 9, 2022 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants