From 7b71b9ce2e84fc9797ed2e2055082ab755c9b5dd Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 15 Sep 2026 12:25:25 +0300 Subject: [PATCH 1/3] Cap auction list page size to stop OOM kills show_all=true set the pagy limit to the full result count, so one request rendered every active auction. A row costs ~266KB of transient objects (~2970 allocations), measured in staging: 13MB for 50 rows, ~260MB for 1000. With a 450Mi container limit and puma running 5 threads, clicking "show all" and then sorting killed the process (exit 137, OOMKilled) and the ingress answered 503. per_page was also unbounded and crashed with 500 on non-numeric input, because params[:per_page] is a String and String#zero? does not exist. Clamp both paths to MAX_PAGE_LIMIT and fall back to the default page size for junk input. This also drops the duplicate COUNT query that the controller ran before pagy did its own. --- app/controllers/auctions_controller.rb | 16 +++++++++----- test/controllers/auctions_controller_test.rb | 23 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/app/controllers/auctions_controller.rb b/app/controllers/auctions_controller.rb index 519da0e98..c6c10a5d1 100644 --- a/app/controllers/auctions_controller.rb +++ b/app/controllers/auctions_controller.rb @@ -4,6 +4,11 @@ class AuctionsController < ApplicationController DEFAULT_PAGE_LIMIT = 15 + # Hard ceiling for one rendered page. A single auction row costs ~270KB of transient + # objects to render, so an unbounded page size (show_all, or a hand-crafted per_page) + # lets one request blow the container memory limit and take the process down. + MAX_PAGE_LIMIT = 100 + # GET /auctions def index set_cors_header @@ -37,12 +42,13 @@ def cors_preflight_check def fetch_auctions_list = Auction.active.search(params, current_user) def per_page_count - count = params[:show_all] == 'true' ? @auctions_list.count : per_page - count = nil if count.zero? - count - end + return MAX_PAGE_LIMIT if params[:show_all] == 'true' - def per_page = params[:per_page] || DEFAULT_PAGE_LIMIT + requested = params[:per_page].to_i + return DEFAULT_PAGE_LIMIT unless requested.positive? + + [requested, MAX_PAGE_LIMIT].min + end def set_cors_header response.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] diff --git a/test/controllers/auctions_controller_test.rb b/test/controllers/auctions_controller_test.rb index 20effe5e9..3ffab90df 100644 --- a/test/controllers/auctions_controller_test.rb +++ b/test/controllers/auctions_controller_test.rb @@ -165,6 +165,29 @@ def test_show_all_with_sorting_returns_every_auction_in_sort_order assert_equal expected_domains, auction_domains end + def test_page_size_is_capped_regardless_of_requested_size + create_extra_active_auctions(AuctionsController::MAX_PAGE_LIMIT + 5) + + get auctions_path, params: { show_all: 'true', sort_by: 'domain_name', sort_direction: 'asc' } + + assert_response :success + assert_select 'tbody#bids tr.contents', AuctionsController::MAX_PAGE_LIMIT + + get auctions_path, params: { per_page: '5000' } + + assert_response :success + assert_select 'tbody#bids tr.contents', AuctionsController::MAX_PAGE_LIMIT + end + + def test_non_numeric_per_page_falls_back_to_the_default_page_size + create_extra_active_auctions(20) + + get auctions_path, params: { per_page: 'all' } + + assert_response :success + assert_select 'tbody#bids tr.contents', AuctionsController::DEFAULT_PAGE_LIMIT + end + private def query_from_link(title) From c0026e5e8a7a98f6223719582ee96d278bf94e2b Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 15 Sep 2026 12:30:15 +0300 Subject: [PATCH 2/3] Bump rubyzip to 3.6.0 for CVE-2026-85396 bundler-audit flagged rubyzip 3.1.0 (path traversal, GHSA-47m2-wp7j-p9vc, fixed in 3.4.0). It is a transitive dependency of selenium-webdriver (rubyzip >= 1.2.2, < 4.0), so only the lockfile entry changes. --- Gemfile.lock | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 07a651bf5..4c9a8ca49 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,6 +180,7 @@ GEM multipart-post (~> 2.0) faraday-net_http (3.4.4) net-http (~> 0.5) + ffi (1.17.4) ffi (1.17.4-aarch64-linux-gnu) ffi (1.17.4-aarch64-linux-musl) ffi (1.17.4-arm-linux-gnu) @@ -305,6 +306,7 @@ GEM nokogiri (~> 1) rake mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) @@ -326,6 +328,9 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -518,7 +523,10 @@ GEM faraday (>= 1) faraday-multipart (>= 1) ruby-progressbar (1.13.0) - rubyzip (3.1.0) + rubyzip (3.6.0) + sass-embedded (1.93.2) + google-protobuf (~> 4.31) + rake (>= 13) sass-embedded (1.93.2-aarch64-linux-gnu) google-protobuf (~> 4.31) sass-embedded (1.93.2-aarch64-linux-musl) @@ -620,6 +628,7 @@ PLATFORMS arm-linux-gnu arm-linux-musl arm64-darwin + ruby x86_64-darwin x86_64-linux-gnu x86_64-linux-musl From 8343884d3d82c5f7e5da1a5c6775cd35fc9471ba Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 15 Sep 2026 12:31:35 +0300 Subject: [PATCH 3/3] Ignore local docs/ and style-guide/ scratch directories --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cfa756a62..eb0227188 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,8 @@ CLAUDE.md /app/assets/builds/* !/app/assets/builds/.keep .cursorindexingignore -.specstory \ No newline at end of file +.specstory + +# Local agent/scratch notes, not part of the repo +/docs/ +/style-guide/ \ No newline at end of file