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