# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/framework/base_support' require 'contrast/agent/reporting/report' module Contrast module Framework module Sinatra # Used when Sinatra is present to define framework specific behavior class Support extend Contrast::Framework::BaseSupport class << self def detection_class 'Sinatra' end def version ::Sinatra::VERSION end def application_name app_class&.cs__name end def application_root app_instance&.root end def server_type 'sinatra' end # Given an object, determine if it is a Sinatra controller with routes. # # @param app [Object] suspected Sinatra app. # @return [Boolean] def sinatra_controller? app # Sinatra is loaded? return false unless defined?(::Sinatra) && defined?(::Sinatra::Base) # App is a subclass of or actually is ::Sinatra::Base. return false unless (app.cs__respond_to?(:<) && app < ::Sinatra::Base) || app == ::Sinatra::Base # App has routes. !app.routes.empty? end # Find all classes that subclass ::Sinatra::Base. Gather their routes. # # @return [Array] def collect_routes return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(::Sinatra) && defined?(::Sinatra::Base) routes = [] sinatra_controllers.each do |controller| controller.routes.each_pair do |method, route_triplets| # Sinatra stores its routes as a triplet: [Mustermann::Sinatra, [], Proc] route_triplets.map(&:first).each do |route_pattern| routes << Contrast::Agent::Reporting::DiscoveredRoute.from_sinatra_route(controller, method, route_pattern) end end end routes end # Given the current request - return a RouteCoverage object # @param request [Contrast::Agent::Request] a contrast tracked request. # @param controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base. # @return [Contrast::Agent::Reporting::RouteCoverage, nil] a Dtm describing the route # matched to the request if a match was found. def current_route_coverage request, controller = ::Sinatra::Base, full_route = nil return unless sinatra_controller?(controller) method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc... # Find route match--checking superclasses if necessary. final_controller, route_pattern = _route_recurse(controller, method, _cleaned_route(request)) return unless final_controller && route_pattern full_route ||= request.env[::Rack::PATH_INFO] new_route_coverage = Contrast::Agent::Reporting::RouteCoverage.new new_route_coverage.attach_rack_based_data(final_controller, method, route_pattern, full_route) new_route_coverage end # Search object space for sinatra controllers--any class that subclasses ::Sinatra::Base. # # @return [Array<::Sinatra::Base>] sinatra controlelrs def sinatra_controllers [::Sinatra::Base] + ObjectSpace.each_object(Class).select { |clazz| sinatra_controller?(clazz) } end def retrieve_request env ::Sinatra::Request.new(env) end # (See BaseSupport#after_load_patches) def after_load_patches return unless defined?(::Sinatra) return unless Gem::Version.new(version) >= Gem::Version.new('3.0.0') Set.new([ Contrast::Agent::Patching::Policy::AfterLoadPatch.new( 'Rack::Protection::EncryptedCookie', 'contrast/framework/sinatra/patch/encrypted_session_cookie', instrumenting_module: 'Contrast::Framework::Sinatra::Patch::EncryptedSessionCookie') ]) end private # Given a controller and a route to match against, find the route_pattern and class that will serve the # route. This is recursive as Sinatra's routing is recursive from subclass to super. # # @param controller [Sinatra::Base, #routes] a Sinatra application. # @param method [::Rack::REQUEST_METHOD] GET, POST, PUT, etc... # @param route [String] the relative route passed from Rack. # @return [Array[Sinatra::Base, Mustermann::Sinatra], nil] Either the controller that # will handle the route along with the route pattern or nil if no match. def _route_recurse controller, method, route return if controller.nil? || controller.cs__class == NilClass route_patterns = controller.routes.fetch(method) { [] }. map(&:first) route_pattern = route_patterns&.find do |matcher| matcher.params(route) # ::Mustermann::Sinatra match. end return controller, route_pattern if route_pattern # Check routes defined in superclass if present. return unless controller.superclass&.instance_variable_get(:@routes) _route_recurse(controller.superclass, method, route) end # Get route and do some cleanup matching that of Sinatra::Base#process_route. # # @param request [Contrast::Agent::Request] a contrast tracked request. # @return [String] the extracted and cleaned relative route. def _cleaned_route request settings = ::Sinatra::Base.settings route = request.env[::Rack::PATH_INFO] return '/' if route.empty? && !settings.empty_path_info? !settings.strict_paths? && route.end_with?('/') ? route[0..-2] : route end # Almost an alias to app_instance. # # @return [::Sinatra::Base] the current controller class as routed by Rack. def app_class return unless defined?(::Sinatra) && defined?(::Sinatra::Base) app_instance.cs__class end # Search the object space for the controller handling this request which will be # the class inheriting from ::Sinatra::Base with @app=nil since it is the final servicer # in the request/middleware chain. # # @return [::Sinatra::Base] the current controller as routed by Rack. def app_instance return unless defined?(::Sinatra) && defined?(::Sinatra::Base) @_app_instance ||= begin sinatra_layers = ObjectSpace.each_object(::Sinatra::Base).to_a sinatra_layers.find { |layer| layer.app.nil? } end end end end end end end