Skip to content

Commit d5662fe

Browse files
committed
[AGILE-176] Harden backlog filter param parsing
Tampered query strings such as `?bucket_ids[0]=5` arrive as a nested `ActionController::Parameters` and crashed `from_params` with an `undefined method 'to_i'` error. Parsing now tokenizes only strings and arrays and ignores nested structures, so malformed input is dropped instead of raising a 500. Serialization joins id lists into the compact comma form, so the generated URLs stay bookmarkable (`?bucket_ids=1,2` rather than the bracketed array form) and passthrough filters render as a single hidden field instead of duplicated `name[]` inputs. https://community.openproject.org/wp/AGILE-176
1 parent c86e79a commit d5662fe

5 files changed

Lines changed: 76 additions & 23 deletions

File tree

modules/backlogs/app/components/backlogs/backlog_filter_select_panel_component.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ def initialize(project:, field_name:)
4747
def filter_fields_for
4848
backlog_filter_params
4949
.except(filter_field)
50-
.flat_map do |name, value|
51-
field_name = value.is_a?(Array) ? "#{name}[]" : name
52-
Array(value).map { |v| [field_name, v, { id: nil }] }
53-
end
50+
.map { |name, value| [name, value, { id: nil }] }
5451
end
5552

5653
def items

modules/backlogs/app/controllers/backlogs/backlog_filters.rb

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,41 @@ module Backlogs
3232
BacklogFilters = Data.define(:bucket_ids, :sprint_ids, :show_all) do
3333
def self.from_params(params)
3434
new(
35-
bucket_ids: Array(params[:bucket_ids]).filter_map do |id|
36-
id == "inbox" ? "inbox" : id.to_i.nonzero?
37-
end.presence,
38-
sprint_ids: Array(params[:sprint_ids]).filter_map { |id| id.to_i.nonzero? }.presence,
35+
bucket_ids: parse_ids(params[:bucket_ids]) { |id| id == "inbox" ? "inbox" : id.to_i.nonzero? },
36+
sprint_ids: parse_ids(params[:sprint_ids]) { |id| id.to_i.nonzero? },
3937
show_all: ActiveRecord::Type::Boolean.new.cast(params[:all]) || false
4038
)
4139
end
4240

41+
# Coerces a raw filter param into a deduplicated list of ids, or nil when
42+
# empty. Accepts both the compact comma-delimited form ("1,2,inbox") and a
43+
# plain array (["1", "2"]). Tampered or nested structures (e.g. the URL
44+
# +?bucket_ids[0]=5+, which arrives as a Hash/ActionController::Parameters)
45+
# are ignored rather than raising.
46+
def self.parse_ids(raw, &)
47+
tokenize(raw).filter_map(&).uniq.presence
48+
end
49+
50+
def self.tokenize(raw)
51+
case raw
52+
when String then raw.split(",")
53+
when Array then raw
54+
else []
55+
end.filter_map { |value| value.to_s.strip.presence }
56+
end
57+
private_class_method :tokenize
58+
4359
def show_inbox?
4460
bucket_ids.nil? || bucket_ids.include?("inbox")
4561
end
4662

63+
# Serializes back to query params, joining id lists into the compact
64+
# comma-delimited form so generated URLs stay bookmarkable
65+
# (+?bucket_ids=1,2+ rather than +?bucket_ids[]=1&bucket_ids[]=2+).
4766
def to_h
4867
result = show_all? ? { all: true } : {}
49-
result[:bucket_ids] = bucket_ids if bucket_ids
50-
result[:sprint_ids] = sprint_ids if sprint_ids
68+
result[:bucket_ids] = bucket_ids.join(",") if bucket_ids
69+
result[:sprint_ids] = sprint_ids.join(",") if sprint_ids
5170
result
5271
end
5372

modules/backlogs/spec/components/backlogs/backlog_filter_select_panel_component_spec.rb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,23 @@ def render_component(field_name:, **params)
8888
describe "hidden filter fields" do
8989
it "passes through sprint_ids when rendering the bucket panel" do
9090
render_component(field_name: :bucket_ids, sprint_ids: ["1"])
91-
expect(page).to have_field("sprint_ids[]", type: :hidden, with: "1", visible: :all)
91+
expect(page).to have_field("sprint_ids", type: :hidden, with: "1", visible: :all)
9292
end
9393

9494
it "passes through bucket_ids when rendering the sprint panel" do
9595
render_component(field_name: :sprint_ids, bucket_ids: ["2"])
96-
expect(page).to have_field("bucket_ids[]", type: :hidden, with: "2", visible: :all)
96+
expect(page).to have_field("bucket_ids", type: :hidden, with: "2", visible: :all)
9797
end
9898

99-
it "expands array values into multiple hidden inputs" do
99+
it "joins array values into a single comma-delimited hidden input" do
100100
render_component(field_name: :sprint_ids, bucket_ids: [1, 2])
101-
expect(page).to have_field("bucket_ids[]", type: :hidden, with: "1", visible: :all)
102-
expect(page).to have_field("bucket_ids[]", type: :hidden, with: "2", visible: :all)
101+
expect(page).to have_field("bucket_ids", type: :hidden, with: "1,2", visible: :all)
103102
end
104103

105-
it "passes through scalar params as a single hidden input without brackets" do
104+
it "renders each passthrough param once per form (clear + filter)" do
106105
render_component(field_name: :sprint_ids, all: true)
107-
# The clear form has 1 `all`, the filter form has 1 `all` and 1 `sprint_ids[]` parameter
108-
expect(page).to have_field(type: :hidden, count: 3, visible: :all)
106+
# The clear form carries 1 `all`, the filter form carries another.
109107
expect(page).to have_field("all", type: :hidden, with: "true", count: 2, visible: :all)
110-
expect(page).to have_field("sprint_ids[]", type: :hidden, count: 1, visible: :all)
111108
end
112109
end
113110
end

modules/backlogs/spec/controllers/backlogs/backlog_filters_spec.rb

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,38 @@
5555
expect(filters.bucket_ids).to eq([1, 2])
5656
end
5757
end
58+
59+
context "when bucket_ids are a comma-delimited string" do
60+
let(:params) { { bucket_ids: "1, 2 ,inbox" } }
61+
62+
it "splits, trims and preserves the inbox sentinel" do
63+
expect(filters.bucket_ids).to eq([1, 2, "inbox"])
64+
end
65+
end
66+
67+
context "when bucket_ids repeat" do
68+
let(:params) { { bucket_ids: "1,1,2" } }
69+
70+
it "deduplicates" do
71+
expect(filters.bucket_ids).to eq([1, 2])
72+
end
73+
end
74+
75+
context "when bucket_ids are a tampered nested structure (?bucket_ids[0]=5)" do
76+
let(:params) { ActionController::Parameters.new(bucket_ids: { "0" => "5" }) }
77+
78+
it "ignores them without raising" do
79+
expect(filters.bucket_ids).to be_nil
80+
end
81+
end
82+
83+
context "when bucket_ids are a tampered array of structures (?bucket_ids[][x]=5)" do
84+
let(:params) { ActionController::Parameters.new(bucket_ids: [{ "x" => "5" }]) }
85+
86+
it "ignores them without raising" do
87+
expect(filters.bucket_ids).to be_nil
88+
end
89+
end
5890
end
5991

6092
describe "#sprint_ids" do
@@ -119,16 +151,24 @@
119151
context "with bucket_ids and sprint_ids" do
120152
let(:params) { { bucket_ids: %w[1 2], sprint_ids: %w[3] } }
121153

122-
it "includes both" do
123-
expect(filters.to_h).to eq({ bucket_ids: [1, 2], sprint_ids: [3] })
154+
it "joins both into compact comma-delimited strings" do
155+
expect(filters.to_h).to eq({ bucket_ids: "1,2", sprint_ids: "3" })
124156
end
125157
end
126158

127159
context "with all params combined" do
128160
let(:params) { { all: "1", bucket_ids: %w[1], sprint_ids: %w[2] } }
129161

130162
it "includes everything" do
131-
expect(filters.to_h).to eq({ all: true, bucket_ids: [1], sprint_ids: [2] })
163+
expect(filters.to_h).to eq({ all: true, bucket_ids: "1", sprint_ids: "2" })
164+
end
165+
end
166+
167+
context "with the inbox sentinel among bucket_ids" do
168+
let(:params) { { bucket_ids: ["5", "inbox"] } }
169+
170+
it "keeps the sentinel in the serialized string" do
171+
expect(filters.to_h).to eq({ bucket_ids: "5,inbox" })
132172
end
133173
end
134174
end

modules/backlogs/spec/helpers/backlogs/common_helper_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
let(:params) { { bucket_ids: %w[1 2], sprint_ids: %w[3] } }
8181

8282
it "includes both in to_h" do
83-
expect(helper.backlog_filters.to_h).to eq({ bucket_ids: [1, 2], sprint_ids: [3] })
83+
expect(helper.backlog_filters.to_h).to eq({ bucket_ids: "1,2", sprint_ids: "3" })
8484
end
8585
end
8686
end

0 commit comments

Comments
 (0)