33require_relative '../../event'
44require_relative '../../trace_keeper'
55require_relative '../../security_event'
6+ require_relative '../../utils/http/media_type'
7+ require_relative '../../utils/http/body'
68
79module Datadog
810 module AppSec
911 module Contrib
1012 module Faraday
1113 # AppSec SSRF detection Middleware for Faraday
1214 class SSRFDetectionMiddleware < ::Faraday ::Middleware
15+ SAMPLE_BODY_KEY = :__datadog_appsec_sample_downstream_body
16+
1317 def call ( env )
1418 context = AppSec . active_context
1519 return @app . call ( env ) unless context && AppSec . rasp_enabled?
1620
17- timeout = Datadog . configuration . appsec . waf_timeout
21+ mark_body_sampling! ( env , context : context )
22+
23+ headers = normalize_headers ( env . request_headers )
24+ # @type var ephemeral_data: ::Datadog::AppSec::Context::input_data
1825 ephemeral_data = {
1926 'server.io.net.url' => env . url . to_s ,
2027 'server.io.net.request.method' => env . method . to_s . upcase ,
21- 'server.io.net.request.headers' => env . request_headers . transform_keys ( & :downcase )
28+ 'server.io.net.request.headers' => headers
2229 }
2330
31+ if env [ SAMPLE_BODY_KEY ] && ( body = parse_body ( env . body , content_type : headers [ 'content-type' ] ) )
32+ ephemeral_data [ 'server.io.net.request.body' ] = body
33+ end
34+
35+ timeout = Datadog . configuration . appsec . waf_timeout
2436 result = context . run_rasp ( Ext ::RASP_SSRF , { } , ephemeral_data , timeout , phase : Ext ::RASP_REQUEST_PHASE )
2537 handle ( result , context : context ) if result . match?
2638
@@ -30,18 +42,44 @@ def call(env)
3042 private
3143
3244 def on_complete ( env , context :)
33- timeout = Datadog . configuration . appsec . waf_timeout
34-
35- response_headers = env . response_headers || { }
45+ headers = normalize_headers ( env . response_headers )
46+ # @type var ephemeral_data: ::Datadog::AppSec::Context::input_data
3647 ephemeral_data = {
3748 'server.io.net.response.status' => env . status . to_s ,
38- 'server.io.net.response.headers' => response_headers . transform_keys ( & :downcase )
49+ 'server.io.net.response.headers' => headers
3950 }
4051
52+ if env [ SAMPLE_BODY_KEY ] && ( body = parse_body ( env . body , content_type : headers [ 'content-type' ] ) )
53+ ephemeral_data [ 'server.io.net.response.body' ] = body
54+ end
55+
56+ timeout = Datadog . configuration . appsec . waf_timeout
4157 result = context . run_rasp ( Ext ::RASP_SSRF , { } , ephemeral_data , timeout , phase : Ext ::RASP_RESPONSE_PHASE )
4258 handle ( result , context : context ) if result . match?
4359 end
4460
61+ def mark_body_sampling! ( env , context :)
62+ max = Datadog . configuration . appsec . api_security . downstream_body_analysis . max_requests
63+ return if context . state [ :downstream_body_analyzed_count ] >= max
64+ return unless context . downstream_body_sampler . sample?
65+
66+ context . state [ :downstream_body_analyzed_count ] += 1
67+ env [ SAMPLE_BODY_KEY ] = true
68+ end
69+
70+ def parse_body ( body , content_type :)
71+ media_type = Utils ::HTTP ::MediaType . parse ( content_type )
72+ return unless media_type
73+
74+ Utils ::HTTP ::Body . parse ( body , media_type : media_type )
75+ end
76+
77+ def normalize_headers ( headers )
78+ return { } if headers . nil? || headers . empty?
79+
80+ headers . transform_keys ( &:downcase )
81+ end
82+
4583 def handle ( result , context :)
4684 AppSec ::Event . tag ( context , result )
4785 TraceKeeper . keep! ( context . trace ) if result . keep?
0 commit comments