Skip to content

Commit 30a52c2

Browse files
authored
Fix IDOR in Kits, BroadcastAnnouncements, and Distributions controllers (#5514)
* Fix IDOR vulnerabilities in Kits, BroadcastAnnouncements, and Distributions controllers Scope all find-by-id queries through current_organization to prevent cross-organization data access (CWE-639). KitsController: deactivate, reactivate, allocations, allocate BroadcastAnnouncementsController: set_broadcast_announcement (edit/update/destroy) DistributionsController: print, show, edit, update, destroy Closes #5509 (partial - 3 of 5 controllers) * Add data-integrity assertion to distribution update cross-org test Verify the distribution record is unchanged after a rejected cross-org update attempt, matching the pattern used in the broadcast announcements spec.
1 parent dd29433 commit 30a52c2

File tree

6 files changed

+114
-10
lines changed

6 files changed

+114
-10
lines changed

app/controllers/broadcast_announcements_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def destroy
4545
private
4646

4747
def set_broadcast_announcement
48-
@broadcast_announcement = BroadcastAnnouncement.find(params[:id])
48+
@broadcast_announcement = BroadcastAnnouncement.where(organization_id: current_organization.id).find(params[:id])
4949
end
5050

5151
def broadcast_announcement_params

app/controllers/distributions_controller.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class DistributionsController < ApplicationController
1414
skip_before_action :require_organization, only: %i(calendar)
1515

1616
def print
17-
@distribution = Distribution.find(params[:id])
17+
@distribution = current_organization.distributions.find(params[:id])
1818
respond_to do |format|
1919
format.any do
2020
pdf = DistributionPdf.new(current_organization, @distribution)
@@ -27,7 +27,8 @@ def print
2727
end
2828

2929
def destroy
30-
service = DistributionDestroyService.new(params[:id])
30+
distribution = current_organization.distributions.find(params[:id])
31+
service = DistributionDestroyService.new(distribution.id)
3132
result = service.call
3233

3334
if result.success?
@@ -169,7 +170,7 @@ def new
169170
end
170171

171172
def show
172-
@distribution = Distribution.includes(:storage_location, line_items: :item).find(params[:id])
173+
@distribution = current_organization.distributions.includes(:storage_location, line_items: :item).find(params[:id])
173174
@line_items = @distribution.line_items
174175

175176
@total_quantity = @distribution.total_quantity
@@ -180,7 +181,7 @@ def show
180181
end
181182

182183
def edit
183-
@distribution = Distribution.includes(:line_items).includes(:storage_location).find(params[:id])
184+
@distribution = current_organization.distributions.includes(:line_items).includes(:storage_location).find(params[:id])
184185
@distribution.initialize_request_items
185186
if (!@distribution.complete? && @distribution.future?) ||
186187
current_user.has_cached_role?(Role::ORG_ADMIN, current_organization)
@@ -203,7 +204,7 @@ def edit
203204
end
204205

205206
def update
206-
@distribution = Distribution.includes(:line_items).includes(:storage_location).find(params[:id])
207+
@distribution = current_organization.distributions.includes(:line_items).includes(:storage_location).find(params[:id])
207208
result = DistributionUpdateService.new(@distribution, distribution_params).call
208209

209210
if result.success?

app/controllers/kits_controller.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ def create
4444
end
4545

4646
def deactivate
47-
@kit = Kit.find(params[:id])
47+
@kit = current_organization.kits.find(params[:id])
4848
@kit.deactivate
4949
redirect_back(fallback_location: dashboard_path, notice: "Kit has been deactivated!")
5050
end
5151

5252
def reactivate
53-
@kit = Kit.find(params[:id])
53+
@kit = current_organization.kits.find(params[:id])
5454
if @kit.can_reactivate?
5555
@kit.reactivate
5656
redirect_back(fallback_location: dashboard_path, notice: "Kit has been reactivated!")
@@ -60,15 +60,15 @@ def reactivate
6060
end
6161

6262
def allocations
63-
@kit = Kit.find(params[:id])
63+
@kit = current_organization.kits.find(params[:id])
6464
@storage_locations = current_organization.storage_locations.active
6565
@inventory = View::Inventory.new(current_organization.id)
6666

6767
load_form_collections
6868
end
6969

7070
def allocate
71-
@kit = Kit.find(params[:id])
71+
@kit = current_organization.kits.find(params[:id])
7272
@storage_location = current_organization.storage_locations.active.find(kit_adjustment_params[:storage_location_id])
7373
@change_by = kit_adjustment_params[:change_by].to_i
7474
begin

spec/requests/broadcast_announcements_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,36 @@
107107
expect(response).to have_http_status(:redirect)
108108
end
109109
end
110+
111+
context "when accessing an announcement from another organization" do
112+
let(:other_organization) { create(:organization) }
113+
let(:other_announcement) {
114+
BroadcastAnnouncement.create!(
115+
expiry: Time.zone.today,
116+
link: "http://example.com",
117+
message: "other org announcement",
118+
user_id: create(:user, organization: other_organization).id,
119+
organization_id: other_organization.id
120+
)
121+
}
122+
123+
it "does not allow editing an announcement from another organization" do
124+
get edit_broadcast_announcement_url(other_announcement)
125+
expect(response.status).to eq(404)
126+
end
127+
128+
it "does not allow updating an announcement from another organization" do
129+
patch broadcast_announcement_url(other_announcement), params: {broadcast_announcement: {message: "hacked"}}
130+
expect(response.status).to eq(404)
131+
expect(other_announcement.reload.message).to eq("other org announcement")
132+
end
133+
134+
it "does not allow destroying an announcement from another organization" do
135+
other_announcement # ensure created
136+
expect {
137+
delete broadcast_announcement_url(other_announcement)
138+
}.not_to change(BroadcastAnnouncement, :count)
139+
expect(response.status).to eq(404)
140+
end
141+
end
110142
end

spec/requests/distributions_requests_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@
5555
end
5656
end
5757

58+
context "when accessing a distribution from another organization" do
59+
it "returns 404" do
60+
other_distribution = create(:distribution, organization: create(:organization))
61+
get print_distribution_path(id: other_distribution.id)
62+
expect(response.status).to eq(404)
63+
end
64+
end
65+
5866
include_examples "restricts access to organization users/admins"
5967
end
6068

@@ -530,6 +538,14 @@
530538
end
531539
end
532540

541+
context "when accessing a distribution from another organization" do
542+
it "returns 404" do
543+
other_distribution = create(:distribution, organization: create(:organization))
544+
get distribution_path(id: other_distribution.id)
545+
expect(response.status).to eq(404)
546+
end
547+
end
548+
533549
include_examples "restricts access to organization users/admins"
534550
end
535551

@@ -727,6 +743,16 @@
727743
end
728744
end
729745

746+
context "when accessing a distribution from another organization" do
747+
it "returns 404" do
748+
other_distribution = create(:distribution, organization: create(:organization))
749+
original_comment = other_distribution.comment
750+
patch distribution_path(id: other_distribution.id), params: {distribution: {comment: "hacked"}}
751+
expect(response.status).to eq(404)
752+
expect(other_distribution.reload.comment).to eq(original_comment)
753+
end
754+
end
755+
730756
include_examples "restricts access to organization users/admins"
731757
end
732758

@@ -947,6 +973,14 @@
947973
end
948974
end
949975

976+
context "when accessing a distribution from another organization" do
977+
it "returns 404" do
978+
other_distribution = create(:distribution, organization: create(:organization))
979+
get edit_distribution_path(id: other_distribution.id)
980+
expect(response.status).to eq(404)
981+
end
982+
end
983+
950984
include_examples "restricts access to organization users/admins"
951985
end
952986

@@ -984,6 +1018,16 @@
9841018
expect(flash[:error]).to eq("We can't delete distributions entered before #{1.day.ago.to_date}.")
9851019
end
9861020

1021+
context "when accessing a distribution from another organization" do
1022+
it "returns 404" do
1023+
other_distribution = create(:distribution, organization: create(:organization))
1024+
expect {
1025+
delete distribution_path(id: other_distribution.id)
1026+
}.not_to change { Distribution.count }
1027+
expect(response.status).to eq(404)
1028+
end
1029+
end
1030+
9871031
include_examples "restricts access to organization users/admins"
9881032
end
9891033
end

spec/requests/kit_requests_spec.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,32 @@
100100
expect(flash[:notice]).to eq("Kit has been reactivated!")
101101
end
102102
end
103+
104+
context "when accessing a kit from another organization" do
105+
let(:other_organization) { create(:organization) }
106+
let(:other_kit) { create_kit(organization: other_organization) }
107+
108+
it "does not allow deactivating a kit from another organization" do
109+
put deactivate_kit_url(other_kit)
110+
expect(response.status).to eq(404)
111+
end
112+
113+
it "does not allow reactivating a kit from another organization" do
114+
other_kit.deactivate
115+
put reactivate_kit_url(other_kit)
116+
expect(response.status).to eq(404)
117+
end
118+
119+
it "does not allow viewing allocations for a kit from another organization" do
120+
get allocations_kit_url(other_kit)
121+
expect(response.status).to eq(404)
122+
end
123+
124+
it "does not allow allocating for a kit from another organization" do
125+
storage_location = create(:storage_location, organization: organization)
126+
post allocate_kit_url(other_kit), params: {kit_adjustment: {storage_location_id: storage_location.id, change_by: 5}}
127+
expect(response.status).to eq(404)
128+
end
129+
end
103130
end
104131
end

0 commit comments

Comments
 (0)