Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fixes of Sprint: F15APR2024-IAST #74

Merged
merged 12 commits into from
May 3, 2024
2 changes: 1 addition & 1 deletion lib/newrelic_security/agent/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def start_event_processor
end

def start_iast_client
@iast_client&.iast_dequeue_thread&.kill
@iast_client&.iast_dequeue_threads&.each { |t| t.kill if t }
@iast_client&.iast_data_transfer_request_processor_thread&.kill
@iast_client = nil
@iast_client = NewRelic::Security::Agent::Control::IASTClient.new
Expand Down
1 change: 1 addition & 0 deletions lib/newrelic_security/agent/configuration/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def detect_framework
return :padrino if defined?(::Padrino)
return :sinatra if defined?(::Sinatra)
return :roda if defined?(::Roda)
return :grape if defined?(::Grape)
end

def generate_uuid
Expand Down
6 changes: 4 additions & 2 deletions lib/newrelic_security/agent/control/collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def collect(case_type, args, event_category = nil, **keyword_args)
find_deserialisation(event, stk) if case_type != REFLECTED_XSS && NewRelic::Security::Agent.config[:'security.detection.deserialization.enabled']
find_rci(event, stk) if case_type != REFLECTED_XSS && NewRelic::Security::Agent.config[:'security.detection.rci.enabled']
event.stacktrace = stk[0..user_frame_index].map(&:to_s)
route = nil
if case_type == REFLECTED_XSS
event.httpResponse[:contentType] = keyword_args[:response_header]
route = NewRelic::Security::Agent::Control::HTTPContext.get_context.route
Expand All @@ -52,7 +53,7 @@ def collect(case_type, args, event_category = nil, **keyword_args)
end
# In rails 5 method name keeps chaning for same api call (ex: _app_views_sqli_sqlinjectionattackcase_html_erb__1999281606898621405_2624809100).
# Hence, considering only frame absolute_path & lineno for apiId calculation.
event.apiId = "#{case_type}-#{calculate_api_id(stk[0..user_frame_index].map { |frame| "#{frame.absolute_path}:#{frame.lineno}" }, event.httpRequest[:method])}"
event.apiId = "#{case_type}-#{calculate_api_id(stk[0..user_frame_index].map { |frame| "#{frame.absolute_path}:#{frame.lineno}" }, event.httpRequest[:method], route)}"
NewRelic::Security::Agent.agent.event_processor.send_event(event)
if event.httpRequest[:headers].key?(NR_CSEC_FUZZ_REQUEST_ID) && event.apiId == event.httpRequest[:headers][NR_CSEC_FUZZ_REQUEST_ID].split(COLON_IAST_COLON)[0]
NewRelic::Security::Agent.agent.iast_client.completed_requests[event.parentId] << event.id
Expand All @@ -78,7 +79,8 @@ def get_user_frame_index(stk)
return -1
end

def calculate_api_id(stk, method)
def calculate_api_id(stk, method, route)
stk << route if route
::Digest::SHA256.hexdigest("#{stk.join(PIPE)}|#{method}").to_s
rescue Exception => e
NewRelic::Security::Agent.logger.error "Exception in calculate_api_id : #{e} #{e.backtrace}"
Expand Down
15 changes: 4 additions & 11 deletions lib/newrelic_security/agent/control/control_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def handle_ic_command(message)
fuzz_request.case_type = message_object[:arguments][1]
NewRelic::Security::Agent.agent.iast_client.pending_request_ids << message_object[:id]
NewRelic::Security::Agent.agent.iast_client.enqueue(fuzz_request)
fuzz_request = nil
when 12
NewRelic::Security::Agent.logger.info "Validator asked to reconnect(CC#12), calling reconnect_at_will"
reconnect_at_will
Expand Down Expand Up @@ -89,18 +90,10 @@ def parse_message(message)
end

def reconnect_at_will
@stop_fuzzing = true
if NewRelic::Security::Agent::Utils.is_IAST?
while NewRelic::Security::Agent.agent.iast_client.fuzzQ && NewRelic::Security::Agent.agent.iast_client.fuzzQ.size > 0
NewRelic::Security::Agent.logger.info "Waiting for fuzzQ to get empty, current size: #{NewRelic::Security::Agent.agent.iast_client.fuzzQ.size}"
sleep 0.1
end
end
NewRelic::Security::Agent.agent.iast_client.fuzzQ.clear
NewRelic::Security::Agent.agent.iast_client.completed_requests.clear
NewRelic::Security::Agent.agent.iast_client.pending_request_ids.clear
NewRelic::Security::Agent.config.disable_security
while NewRelic::Security::Agent.agent.event_processor.eventQ && NewRelic::Security::Agent.agent.event_processor.eventQ.size > 0
NewRelic::Security::Agent.logger.info "Waiting for eventQ to get empty, current size: #{NewRelic::Security::Agent.agent.event_processor.eventQ.size}"
sleep 0.1
end
Thread.new { NewRelic::Security::Agent.agent.reconnect(0) }
end

Expand Down
1 change: 0 additions & 1 deletion lib/newrelic_security/agent/control/http_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def initialize(env)
strio.rewind
@body = @body.force_encoding(Encoding::UTF_8) if @body.is_a?(String)
@cache = Hash.new
@route = "#{env[REQUEST_METHOD].to_s}@#{env[PATH_INFO].to_s}"
NewRelic::Security::Agent.agent.http_request_count.increment
NewRelic::Security::Agent.agent.iast_client.completed_requests[@headers[NR_CSEC_PARENT_ID]] = [] if @headers.key?(NR_CSEC_PARENT_ID)
end
Expand Down
35 changes: 21 additions & 14 deletions lib/newrelic_security/agent/control/iast_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'json'
require 'uri'
require 'set'
require 'resolv'
require 'resolv-replace'

module NewRelic::Security
module Agent
Expand All @@ -16,11 +18,10 @@ module Control

class IASTClient

attr_reader :fuzzQ, :iast_dequeue_thread
attr_reader :fuzzQ, :iast_dequeue_threads
attr_accessor :cooldown_till_timestamp, :last_fuzz_cc_timestamp, :pending_request_ids, :completed_requests, :iast_data_transfer_request_processor_thread

def initialize
@http = nil
@fuzzQ = ::SizedQueue.new(FUZZQ_QUEUE_SIZE)
@cooldown_till_timestamp = current_time_millis
@last_fuzz_cc_timestamp = current_time_millis
Expand All @@ -40,12 +41,15 @@ def enqueue(message)

def create_dequeue_threads
# TODO: Create 3 or more consumers for event sending
@iast_dequeue_thread = Thread.new do
Thread.current.name = "newrelic_security_iast_thread"
loop do
fuzz_request = @fuzzQ.deq #thread blocks when the queue is empty
fire_request(fuzz_request.id, fuzz_request.request)
fuzz_request = nil
@iast_dequeue_threads = []
3.times do |t|
@iast_dequeue_threads << Thread.new do
Thread.current.name = "newrelic_security_iast_thread-#{t}"
loop do
fuzz_request = @fuzzQ.deq #thread blocks when the queue is empty
fire_request(fuzz_request.id, fuzz_request.request)
fuzz_request = nil
end
end
end
rescue Exception => exception
Expand All @@ -69,7 +73,7 @@ def create_iast_data_transfer_request_processor
if batch_size > 100 && remaining_record_capacity > batch_size
iast_data_transfer_request = NewRelic::Security::Agent::Control::IASTDataTransferRequest.new
iast_data_transfer_request.batchSize = batch_size * 2
iast_data_transfer_request.pendingRequestIds = pending_request_ids
iast_data_transfer_request.pendingRequestIds = pending_request_ids.to_a
iast_data_transfer_request.completedRequests = completed_requests
NewRelic::Security::Agent.agent.event_processor.send_iast_data_transfer_request(iast_data_transfer_request)
end
Expand All @@ -84,17 +88,20 @@ def current_time_millis
end

def fire_request(fuzz_request_id, request)
unless @http
@http = ::Net::HTTP.new('localhost', NewRelic::Security::Agent.config[:listen_port])
@http.open_timeout = 5
unless ::Thread.current[:http]
Thread.current[:http] = ::Net::HTTP.new('127.0.0.1', NewRelic::Security::Agent.config[:listen_port])
Thread.current[:http].open_timeout = 5
end
request[HEADERS].delete(VERSION) if request[HEADERS].key?(VERSION)
response = @http.send_request(request[METHOD], ::URI.parse(request[URL]).to_s, request[BODY], request[HEADERS])
NewRelic::Security::Agent.logger.debug "IAST fuzz request : #{request.inspect} \nresponse: #{response.inspect}\n"
time_before_request = (Time.now.to_f * 1000).to_i
response = Thread.current[:http].send_request(request[METHOD], ::URI.parse(request[URL]).to_s, request[BODY], request[HEADERS])
time_after_request = (Time.now.to_f * 1000).to_i
NewRelic::Security::Agent.logger.debug "IAST fuzz request : time taken : #{time_after_request - time_before_request}ms, #{request.inspect} \nresponse: #{response.inspect}\n"
rescue Exception => exception
NewRelic::Security::Agent.logger.debug "Unable to fire IAST fuzz request : #{exception.inspect} #{exception.backtrace}, sending fuzzfail event for #{request.inspect}\n"
NewRelic::Security::Agent::Utils.create_fuzz_fail_event(request[HEADERS][NR_CSEC_FUZZ_REQUEST_ID])
ensure
NewRelic::Security::Agent.agent.iast_client.completed_requests[fuzz_request_id] = []
NewRelic::Security::Agent.agent.iast_client.pending_request_ids.delete(fuzz_request_id)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize
@jsonName = :'iast-data-request'
@applicationUUID = NewRelic::Security::Agent.config[:uuid]
@batchSize = 10
@pendingRequestIds = ::Set.new
@pendingRequestIds = []
@completedRequests = Hash.new
end

Expand Down
2 changes: 1 addition & 1 deletion lib/newrelic_security/agent/control/reflected_xss.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module ReflectedXSS

def check_xss(http_req, retval)
# TODO: Check if enableHTTPRequestPrinting is required.
return unless http_req
return if http_req.nil? || retval.empty?
if retval[1].key?(Content_Type) && (retval[1][Content_Type].start_with?(*UNSUPPORTED_MEDIA_TYPES) || retval[1][Content_Type].start_with?(*UNSUPPORTED_CONTENT_TYPES))
return
end
Expand Down
2 changes: 2 additions & 0 deletions lib/newrelic_security/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ module NewRelic::Security
DELETE = 'delete'
WRITE = 'write'
BINWRITE = 'binwrite'
REQUEST_METHOD = 'REQUEST_METHOD'
PATH_INFO = 'PATH_INFO'
CONTENT_TYPE = 'CONTENT_TYPE'
REQUEST_URI = 'REQUEST_URI'
SERVER_PORT = 'SERVER_PORT'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ def call_on_enter(env)
NewRelic::Security::Agent.logger.debug "OnEnter : #{self.class}.#{__method__}"
NewRelic::Security::Agent.config.update_port = NewRelic::Security::Agent::Utils.app_port(env) unless NewRelic::Security::Agent.config[:listen_port]
NewRelic::Security::Agent::Utils.get_app_routes(:padrino) if NewRelic::Security::Agent.agent.route_map.empty?
NewRelic::Security::Agent::Control::HTTPContext.set_context(env.instance_variable_get(:@env))
extracted_env = env.instance_variable_get(:@env)
NewRelic::Security::Agent::Control::HTTPContext.set_context(extracted_env)
ctxt = NewRelic::Security::Agent::Control::HTTPContext.get_context
ctxt.route = "#{extracted_env[REQUEST_METHOD].to_s}@#{extracted_env[PATH_INFO].to_s}" if ctxt
NewRelic::Security::Agent::Utils.parse_fuzz_header
rescue => exception
NewRelic::Security::Agent.logger.error "Exception in hook in #{self.class}.#{__method__}, #{exception.inspect}, #{exception.backtrace}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def _roda_handle_main_route_on_enter(env)
NewRelic::Security::Agent.config.update_port = NewRelic::Security::Agent::Utils.app_port(env) unless NewRelic::Security::Agent.config[:listen_port]
NewRelic::Security::Agent::Utils.get_app_routes(:roda) if NewRelic::Security::Agent.agent.route_map.empty?
NewRelic::Security::Agent::Control::HTTPContext.set_context(env)
ctxt = NewRelic::Security::Agent::Control::HTTPContext.get_context
ctxt.route = "#{env[REQUEST_METHOD].to_s}@#{env[PATH_INFO].to_s}" if ctxt
NewRelic::Security::Agent::Utils.parse_fuzz_header
rescue => exception
NewRelic::Security::Agent.logger.error "Exception in hook in #{self.class}.#{__method__}, #{exception.inspect}, #{exception.backtrace}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def execute_on_enter(sql, bind_vars, *args)
NewRelic::Security::Agent.logger.debug "OnEnter : #{self.class}.#{__method__}"
hash = {}
hash[:sql] = sql
hash[:parameters] = bind_vars.map(&:to_s)
hash[:parameters] = bind_vars.is_a?(String) ? [bind_vars] : bind_vars.map(&:to_s)
hash[:parameters] = hash[:parameters] + args unless args.empty?
event = NewRelic::Security::Agent::Control::Collector.collect(SQL_DB_COMMAND, [hash], SQLITE) unless NewRelic::Security::Instrumentation::InstrumentationUtils.sql_filter_events?(hash[:sql])
rescue => exception
NewRelic::Security::Agent.logger.error "Exception in hook in #{self.class}.#{__method__}, #{exception.inspect}, #{exception.backtrace}"
Expand Down Expand Up @@ -56,7 +57,8 @@ def execute_batch_on_enter(sql, bind_vars, *args)
NewRelic::Security::Agent.logger.debug "OnEnter : #{self.class}.#{__method__}"
hash = {}
hash[:sql] = sql
hash[:parameters] = bind_vars.map(&:to_s)
hash[:parameters] = bind_vars.is_a?(String) ? [bind_vars] : bind_vars.map(&:to_s)
hash[:parameters] = hash[:parameters] + args unless args.empty?
event = NewRelic::Security::Agent::Control::Collector.collect(SQL_DB_COMMAND, [hash], SQLITE) unless NewRelic::Security::Instrumentation::InstrumentationUtils.sql_filter_events?(hash[:sql])
rescue => exception
NewRelic::Security::Agent.logger.error "Exception in hook in #{self.class}.#{__method__}, #{exception.inspect}, #{exception.backtrace}"
Expand Down
Loading