Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a dependency for the changes?

Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ end
gem 'mysql2'

# Webserver - included in development and test and optionally in production
gem 'puma'
gem 'puma', '>= 6.4.3'

gem 'bootsnap', require: false
gem 'csv'
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ DEPENDENCIES
net-smtp
oauth2
pdf-reader
puma
puma (>= 6.4.3)
rack-cors
rails (~> 8.0)
rails-latex
Expand Down
56 changes: 56 additions & 0 deletions app/middleware/request_smuggling_protection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'json'

#
# Rejects ambiguous HTTP framing headers to reduce request smuggling risk.
#
class RequestSmugglingProtection
BAD_REQUEST_RESPONSE = [
400,
{ 'Content-Type' => 'application/json', 'Connection' => 'close' },
[{ error: 'Malformed request framing headers' }.to_json]
].freeze

def initialize(app)
@app = app
end

def call(env)
return BAD_REQUEST_RESPONSE if malformed_framing_headers?(env)

@app.call(env)
end

private

def malformed_framing_headers?(env)
content_length = env['CONTENT_LENGTH']
transfer_encoding = env['HTTP_TRANSFER_ENCODING']

conflicting_content_length_and_transfer_encoding?(content_length, transfer_encoding) ||
invalid_content_length?(content_length) ||
invalid_transfer_encoding?(transfer_encoding)
end

def conflicting_content_length_and_transfer_encoding?(content_length, transfer_encoding)
header_present?(content_length) && header_present?(transfer_encoding)
end

def invalid_content_length?(content_length)
return false unless header_present?(content_length)

content_length.include?(',') || content_length !~ /\A\d+\z/
end

def invalid_transfer_encoding?(transfer_encoding)
return false unless header_present?(transfer_encoding)

normalized = transfer_encoding.split(',').map(&:strip).reject(&:empty?)
normalized != ['chunked']
end

def header_present?(value)
!value.nil? && !value.strip.empty?
end
end
3 changes: 3 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
require 'csv'
require 'yaml'
require 'bunny-pub-sub/services_manager'
require_relative '../app/middleware/request_smuggling_protection'


# Precompile assets before deploying to production
if defined?(Bundler)
Expand Down Expand Up @@ -250,6 +252,7 @@ def self.fetch_boolean_env(name)
resource '*', headers: :any, methods: %i(get post put delete options)
end
end
config.middleware.insert_before Rack::Cors, RequestSmugglingProtection

config.active_support.to_time_preserves_timezone = :zone

Expand Down
16 changes: 16 additions & 0 deletions config/puma.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }

workers ENV.fetch("WEB_CONCURRENCY", 2)
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
preload_app!
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV", "development")

# SECURITY FIX: Raise on SIGTERM to prevent request queue poisoning
raise_exception_on_sigterm

# SECURITY FIX: Reject malformed/early-hint requests
# Forces Puma 6.x strict HTTP parsing mode
lowlevel_error_handler do |err, env, status|
[400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
end

# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
Expand Down