# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' require 'contrast/components/scope' module Contrast module Framework module Rack module Patch # Our patch into the Rack::Session::Cookie Class, allowing for the # runtime detection of insecure configurations on individual cookies # within the application class SessionCookie extend Contrast::Components::Logger::InstanceMethods extend Contrast::Components::Scope::InstanceMethods CS__SECURE_RULE_NAME = 'secure-flag-missing' CS__HTTPONLY_NAME = 'rails-http-only-disabled' CS__SESSION_TIMEOUT_NAME = 'session-timeout' SAFE_SESSION_TIMEOUT = (30 * 60 * 60) class << self include Contrast::Utils::InvalidConfigurationUtil def instrument @_instrument ||= begin ::Rack::Session::Cookie.class_eval do alias_method(:cs__patched_initialize, :initialize) def initialize app, options = {} # rubocop:disable Style/OptionHash Contrast::Framework::Rack::Patch::SessionCookie.analyze(options) cs__patched_initialize(app, options) end end true end end def analyze options return unless ::Contrast::AGENT.enabled? return if ::Contrast::ASSESS.forcibly_disabled? apply_session_timeout(options) apply_httponly(options) apply_secure_session(options) end private def vulnerable_setting?(setting_key, safe_settings_value, options, safe_default: true, comparison_type: nil) # In most cases, Rack is pretty nice and the default value is safe return !safe_default unless options&.key?(setting_key) value = options[setting_key] return value.to_i > safe_settings_value.to_i if comparison_type&.to_sym == :greater_than value != safe_settings_value end def apply_secure_session options return unless vulnerable_setting?(:secure, true, options, safe_default: false) cs__report_finding(CS__SECURE_RULE_NAME, options, caller_locations(10, 9)[0]) rescue StandardError => e begin logger.error('Unable to track call to secure session', e) rescue StandardError nil end end def apply_session_timeout options return unless vulnerable_setting?(:expire_after, SAFE_SESSION_TIMEOUT, options, safe_default: false, comparison_type: :greater_than) cs__report_finding(CS__SESSION_TIMEOUT_NAME, options, caller_locations(10, 9)[0]) rescue StandardError => e begin logger.error('Unable to track call to set session timeout', e) rescue StandardError nil end end def apply_httponly options return unless vulnerable_setting?(:httponly, true, options) cs__report_finding(CS__HTTPONLY_NAME, options, caller_locations(10, 9)[0]) rescue StandardError => e begin logger.error('Unable to track call to httponly', e) rescue StandardError nil end end end end end end end end