Skip to content

Commit 2c8c7a9

Browse files
committed
Improve rack spoof handling and split integration setup
1 parent 02c4f3e commit 2c8c7a9

File tree

4 files changed

+66
-20
lines changed

4 files changed

+66
-20
lines changed

lib/log_struct/integrations.rb

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,25 @@ def self.setup_initializers(railtie)
3838
end
3939
end
4040

41-
sig { void }
42-
def self.setup_integrations
41+
sig { params(stage: Symbol).void }
42+
def self.setup_integrations(stage: :all)
4343
config = LogStruct.config
4444

45-
# Set up each integration with consistent configuration pattern
45+
case stage
46+
when :non_middleware
47+
setup_non_middleware_integrations(config)
48+
when :middleware
49+
setup_middleware_integrations(config)
50+
when :all
51+
setup_non_middleware_integrations(config)
52+
setup_middleware_integrations(config)
53+
else
54+
raise ArgumentError, "Unknown integration stage: #{stage}"
55+
end
56+
end
57+
58+
sig { params(config: LogStruct::Configuration).void }
59+
def self.setup_non_middleware_integrations(config)
4660
Integrations::Lograge.setup(config) if config.integrations.enable_lograge
4761
Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
4862
Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
@@ -51,8 +65,6 @@ def self.setup_integrations
5165
Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
5266
Integrations::Ahoy.setup(config) if config.integrations.enable_ahoy
5367
Integrations::ActiveModelSerializers.setup(config) if config.integrations.enable_active_model_serializers
54-
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
55-
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
5668
Integrations::Shrine.setup(config) if config.integrations.enable_shrine
5769
Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
5870
Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
@@ -62,5 +74,13 @@ def self.setup_integrations
6274
end
6375
Integrations::Puma.setup(config) if config.integrations.enable_puma
6476
end
77+
78+
sig { params(config: LogStruct::Configuration).void }
79+
def self.setup_middleware_integrations(config)
80+
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
81+
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
82+
end
83+
84+
private_class_method :setup_non_middleware_integrations, :setup_middleware_integrations
6585
end
6686
end

lib/log_struct/integrations/rack_error_handler.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def self.setup(config)
1919
return nil unless config.integrations.enable_rack_error_handler
2020

2121
# Add structured logging middleware for security violations and errors
22-
# Need to insert after ShowExceptions to catch IP spoofing errors
23-
::Rails.application.middleware.insert_after(
24-
::ActionDispatch::ShowExceptions,
22+
# Need to insert before RemoteIp to catch IP spoofing errors it raises
23+
::Rails.application.middleware.insert_before(
24+
::ActionDispatch::RemoteIp,
2525
Integrations::RackErrorHandler::Middleware
2626
)
2727

lib/log_struct/integrations/rack_error_handler/middleware.rb

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,14 @@ def initialize(app)
5555
def call(env)
5656
return @app.call(env) unless LogStruct.enabled?
5757

58-
# Try to process the request
58+
request = ::ActionDispatch::Request.new(env)
59+
5960
begin
61+
# Trigger the same spoofing checks that ActionDispatch::RemoteIp performs after
62+
# it is initialized in the middleware stack. We run this manually because we
63+
# execute before that middleware and still want spoofing attacks to surface here.
64+
perform_remote_ip_check!(request)
65+
6066
@app.call(env)
6167
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
6268
# Create a security log for IP spoofing
@@ -65,7 +71,7 @@ def call(env)
6571
http_method: env["REQUEST_METHOD"],
6672
user_agent: env["HTTP_USER_AGENT"],
6773
referer: env["HTTP_REFERER"],
68-
request_id: env["action_dispatch.request_id"],
74+
request_id: request.request_id,
6975
message: ip_spoof_error.message,
7076
client_ip: env["HTTP_CLIENT_IP"],
7177
x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
@@ -74,13 +80,7 @@ def call(env)
7480

7581
::Rails.logger.warn(security_log)
7682

77-
# Report the error
78-
context = extract_request_context(env)
79-
LogStruct.handle_exception(ip_spoof_error, source: Source::Security, context: context)
80-
81-
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
82-
# If we are only logging or reporting these security errors, then return a default response
83-
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS, [IP_SPOOF_HTML]]
83+
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
8484
rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
8585
# Create a security log for CSRF error
8686
request = ::ActionDispatch::Request.new(env)
@@ -102,7 +102,7 @@ def call(env)
102102

103103
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
104104
# If we are only logging or reporting these security errors, then return a default response
105-
[FORBIDDEN_STATUS, CSRF_HEADERS, [CSRF_HTML]]
105+
[FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
106106
rescue => error
107107
# Extract request context for error reporting
108108
context = extract_request_context(env)
@@ -119,6 +119,23 @@ def call(env)
119119

120120
private
121121

122+
sig { params(request: ::ActionDispatch::Request).void }
123+
def perform_remote_ip_check!(request)
124+
action_dispatch_config = ::Rails.application.config.action_dispatch
125+
126+
remote_ip_middleware = ::ActionDispatch::RemoteIp.new(
127+
->(_env) { [200, {}, []] },
128+
action_dispatch_config.ip_spoofing_check,
129+
action_dispatch_config.trusted_proxies
130+
)
131+
132+
return unless remote_ip_middleware.check_ip
133+
134+
::ActionDispatch::RemoteIp::GetIp
135+
.new(request, remote_ip_middleware.check_ip, remote_ip_middleware.proxies)
136+
.to_s
137+
end
138+
122139
sig { params(env: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
123140
def extract_request_context(env)
124141
request = ::ActionDispatch::Request.new(env)

lib/log_struct/railtie.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,21 @@ class Railtie < ::Rails::Railtie
2525
# Merge Rails filter parameters into our filters
2626
LogStruct.merge_rails_filter_parameters!
2727

28-
# Set up all integrations
29-
Integrations.setup_integrations
28+
# Set up non-middleware integrations first
29+
Integrations.setup_integrations(stage: :non_middleware)
3030

3131
# Note: Host allowances are managed by the test app itself.
3232
end
3333

34+
# Setup middleware integrations during Rails configuration (before middleware stack is built)
35+
# Must be done in the Railtie class body, not in an initializer
36+
initializer "logstruct.configure_middleware", before: :build_middleware_stack do |app|
37+
# This runs before middleware stack is frozen, so we can configure it
38+
next unless LogStruct.enabled?
39+
40+
Integrations.setup_integrations(stage: :middleware)
41+
end
42+
3443
# Emit Puma lifecycle logs when running `rails server`
3544
initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
3645
is_server = ::LogStruct.server_mode?

0 commit comments

Comments
 (0)