Module: Datadog::AppSec::APISecurity::RouteExtractor

Defined in:
lib/datadog/appsec/api_security/route_extractor.rb

Overview

This is a helper module to extract the route pattern from the Rack::Request.

Constant Summary collapse

SINATRA_ROUTE_KEY =
'sinatra.route'
SINATRA_ROUTE_SEPARATOR =
' '
GRAPE_ROUTE_KEY =
'grape.routing_args'
RAILS_ROUTE_URI_PATTERN_KEY =
'action_dispatch.route_uri_pattern'
RAILS_ROUTE_KEY =

Rails 8.1.1+

'action_dispatch.route'
RAILS_ROUTES_KEY =
'action_dispatch.routes'
RAILS_PATH_PARAMS_KEY =
'action_dispatch.request.path_parameters'
RAILS_FORMAT_SUFFIX =
'(.:format)'

Class Method Summary collapse

Class Method Details

.route_pattern(request) ⇒ Object

HACK: We rely on the fact that each contrib will modify ‘request.env`

and store information sufficient to compute the canonical
route (ex: `/users/:id`).

When contribs like Sinatra or Grape are used, they could be mounted
into the Rails app, hence you can see the use of the `script_name`
that will contain the path prefix of the mounted app.

Rack
  does not support named arguments, so we have to use `path`
Sinatra
  uses `sinatra.route` with a string like "GET /users/:id"
Grape
  uses `grape.routing_args` with a hash with a `:route_info` key
  that contains a `Grape::Router::Route` object that contains
  `Grape::Router::Pattern` object with an `origin` method
Rails < 7.1 (slow path)
  uses `action_dispatch.routes` to store `ActionDispatch::Routing::RouteSet`
  which can recognize requests
Rails > 7.1 (fast path)
  uses `action_dispatch.route_uri_pattern` with a string like
  "/users/:id(.:format)"
Rails > 8.1.1 (fast path)
  uses `action_dispatch.route` to store the ActionDispatch::Journey::Route
  that matched when the request was routed

WARNING: This method works only after the request has been routed.

WARNING: In Rails > 7.1 when a route was not found,

`action_dispatch.route_uri_pattern` will not be set.
In Rails < 7.1 it also will not be set even if a route was found,
but in this case `action_dispatch.request.path_parameters` won't be empty.


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/datadog/appsec/api_security/route_extractor.rb', line 51

def self.route_pattern(request)
  if request.env.key?(GRAPE_ROUTE_KEY)
    pattern = request.env[GRAPE_ROUTE_KEY][:route_info]&.pattern&.origin
    "#{request.script_name}#{pattern}"
  elsif request.env.key?(SINATRA_ROUTE_KEY)
    pattern = request.env[SINATRA_ROUTE_KEY].split(SINATRA_ROUTE_SEPARATOR, 2)[1]
    "#{request.script_name}#{pattern}"
  elsif request.env.key?(RAILS_ROUTE_KEY)
    request.env[RAILS_ROUTE_KEY].path.spec.to_s.delete_suffix(RAILS_FORMAT_SUFFIX)
  elsif request.env.key?(RAILS_ROUTE_URI_PATTERN_KEY)
    request.env[RAILS_ROUTE_URI_PATTERN_KEY].delete_suffix(RAILS_FORMAT_SUFFIX)
  elsif request.env.key?(RAILS_ROUTES_KEY) && !request.env.fetch(RAILS_PATH_PARAMS_KEY, {}).empty?
    # NOTE: In Rails < 7.1 this `request` argument will be a Rack::Request,
    #       it does not have all the methods that ActionDispatch::Request has.
    #       Before trying to use the router to recognize the route, we need to
    #       create a new ActionDispatch::Request from the request env
    #
    # NOTE: Rails mutates HEAD request by changing the method to GET
    #       and uses it for route recognition to check if the route is defined
    request = request.env[RAILS_ROUTES_KEY].request_class.new(request.env)

    pattern = request.env[RAILS_ROUTES_KEY].router
      .recognize(request) { |route, _| break route.path.spec.to_s }

    # NOTE: If rails is unable to recognize request it returns empty Array
    pattern = nil if pattern&.empty?

    # NOTE: If rails can't recognize the request, we are going to fallback
    #       to generic request path
    (pattern || request.path).delete_suffix(RAILS_FORMAT_SUFFIX)
  else
    Tracing::Contrib::Rack::RouteInference.read_or_infer(request.env)
  end
rescue => e
  AppSec.telemetry&.report(e, description: 'AppSec: Could not extract route pattern')

  nil
end