From a1ea9b207dba811a80f6726f01b15076b298ab77 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 4 Apr 2026 14:25:07 +1100 Subject: [PATCH 1/5] fix(security): harden Puma against HTTP Request Smuggling (CVE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump Puma to >= 6.4.3 in Gemfile for hardened HTTP parsing - Add raise_exception_on_sigterm! to config/puma.rb to prevent request queue poisoning on SIGTERM Severity: SIGNIFICANT Ref: FIX 2 — HTTP Request Smuggling --- Gemfile | 2 +- config/puma.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 90b4021f63..0f70db5a3b 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/config/puma.rb b/config/puma.rb index d9b3e836cf..696492657a 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -32,6 +32,16 @@ # # 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 + # 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 From 8b8731cfbf79c28f6a135b0c17686f546fe6667b Mon Sep 17 00:00:00 2001 From: AjayPAnand Date: Thu, 23 Apr 2026 12:00:11 +1000 Subject: [PATCH 2/5] feat(dev-infra): add nginx config for request smuggling tests update proxy-nginx configuration to support HTTP request smuggling testing scenarios. enable relaxed header handling, adjust client timeouts, and configure proxy connection and buffering behaviour to allow controlled testing conditions. no production behaviour intended; changes are for security testing --- .../request_smuggling_protection.rb | 56 +++++++++++++++++++ config/application.rb | 3 + config/puma.rb | 6 ++ db/schema.rb | 4 +- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 app/middleware/request_smuggling_protection.rb diff --git a/app/middleware/request_smuggling_protection.rb b/app/middleware/request_smuggling_protection.rb new file mode 100644 index 0000000000..46f4be7f8e --- /dev/null +++ b/app/middleware/request_smuggling_protection.rb @@ -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 diff --git a/config/application.rb b/config/application.rb index fe280a2899..f81483010a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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) @@ -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 diff --git a/config/puma.rb b/config/puma.rb index 696492657a..e6c5096670 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -42,6 +42,12 @@ # 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 diff --git a/db/schema.rb b/db/schema.rb index cf1a8adccb..1e49bb2022 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_03_09_065302) do +ActiveRecord::Schema[8.0].define(version: 2026_03_27_041457) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -328,6 +328,7 @@ t.integer "portfolio_generation_pid" t.integer "spec_con_days", default: 0, null: false t.bigint "assessor_id" + t.datetime "portfolio_submission_date" t.index ["assessor_id"], name: "index_projects_on_assessor_id" t.index ["campus_id"], name: "index_projects_on_campus_id" t.index ["enrolled"], name: "index_projects_on_enrolled" @@ -742,6 +743,7 @@ t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false t.integer "feedback_warning_threshold_days", default: 5 t.integer "feedback_overflow_threshold_days", default: 7 + t.boolean "enforce_feedback_before_discussed_in_class", default: false, null: false t.index ["draft_task_definition_id"], name: "index_units_on_draft_task_definition_id" t.index ["main_convenor_id"], name: "index_units_on_main_convenor_id" t.index ["overseer_image_id"], name: "index_units_on_overseer_image_id" From bf5b8b1a1beadde226d682d42090651303814e31 Mon Sep 17 00:00:00 2001 From: BigGeorge99 Date: Fri, 1 May 2026 14:46:35 +1000 Subject: [PATCH 3/5] Revert schema.rb to match 10.0.x --- Gemfile.lock | 2 +- db/schema.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6bb903f2a6..8c2a326e18 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -592,7 +592,7 @@ DEPENDENCIES net-smtp oauth2 pdf-reader - puma + puma (>= 6.4.3) rack-cors rails (~> 8.0) rails-latex diff --git a/db/schema.rb b/db/schema.rb index 675c023189..1e49bb2022 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_03_22_230239) do +ActiveRecord::Schema[8.0].define(version: 2026_03_27_041457) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false From 5381f58cb2f82f2d4199252c62aefc5c9823eea8 Mon Sep 17 00:00:00 2001 From: AjayPAnand <40824905+AjayPAnand@users.noreply.github.com> Date: Fri, 1 May 2026 15:11:48 +1000 Subject: [PATCH 4/5] Reverting schema.rb -removed enforce_feedback_before_discussed_in_class column Removed the 'enforce_feedback_before_discussed_in_class' column from the schema as per the fix that is required this change is not necessary for my PR. --- db/schema.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 1e49bb2022..3ccbad80d5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -743,7 +743,6 @@ t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false t.integer "feedback_warning_threshold_days", default: 5 t.integer "feedback_overflow_threshold_days", default: 7 - t.boolean "enforce_feedback_before_discussed_in_class", default: false, null: false t.index ["draft_task_definition_id"], name: "index_units_on_draft_task_definition_id" t.index ["main_convenor_id"], name: "index_units_on_main_convenor_id" t.index ["overseer_image_id"], name: "index_units_on_overseer_image_id" From b31a32541d57a7afae38764d5237955a004ef46b Mon Sep 17 00:00:00 2001 From: AjayPAnand <40824905+AjayPAnand@users.noreply.github.com> Date: Fri, 1 May 2026 15:15:23 +1000 Subject: [PATCH 5/5] Reverting schema.rb Reverting schema.rb --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 3ccbad80d5..73fc5ebd58 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_03_27_041457) do +ActiveRecord::Schema[8.0].define(version: 2026_03_22_230239) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false