Skip to content

Commit 2353757

Browse files
authored
Merge pull request #638 from alphagov/apply-constraints-bulk-signature-ops
Harden bulk signature actions against attack
2 parents bf0d14c + 85850cc commit 2353757

6 files changed

Lines changed: 154 additions & 22 deletions

File tree

app/controllers/admin/signatures_controller.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
class Admin::SignaturesController < Admin::AdminController
2+
include BulkVerification
3+
24
before_action :fetch_signature, except: [:index, :bulk_validate, :bulk_invalidate, :bulk_destroy]
35
before_action :fetch_signatures, only: [:index]
46

@@ -10,7 +12,7 @@ def index
1012

1113
def bulk_validate
1214
begin
13-
Signature.validate!(signature_ids)
15+
Signature.validate!(selected_ids)
1416
redirect_to admin_signatures_url(q: params[:q]), notice: :signatures_validated
1517
rescue StandardError => e
1618
Appsignal.send_exception e
@@ -30,7 +32,7 @@ def validate
3032

3133
def bulk_invalidate
3234
begin
33-
Signature.invalidate!(signature_ids)
35+
Signature.invalidate!(selected_ids)
3436
redirect_to admin_signatures_url(q: params[:q]), notice: :signatures_invalidated
3537
rescue StandardError => e
3638
Appsignal.send_exception e
@@ -50,7 +52,7 @@ def invalidate
5052

5153
def bulk_destroy
5254
begin
53-
Signature.destroy!(signature_ids)
55+
Signature.destroy!(selected_ids)
5456
redirect_to admin_signatures_url(q: params[:q]), notice: :signatures_deleted
5557
rescue StandardError => e
5658
Appsignal.send_exception e
@@ -75,8 +77,4 @@ def fetch_signatures
7577
def fetch_signature
7678
@signature = Signature.find(params[:id])
7779
end
78-
79-
def signature_ids
80-
params[:ids].to_s.split(",").map(&:to_i).reject(&:zero?)
81-
end
8280
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module BulkVerification
2+
extend ActiveSupport::Concern
3+
4+
class InvalidBulkRequest < RuntimeError; end
5+
6+
included do
7+
before_action :verify_bulk_request, if: :bulk_request?
8+
9+
helper_method :bulk_verifier
10+
11+
rescue_from ActiveSupport::MessageVerifier::InvalidSignature do
12+
raise BulkVerification::InvalidBulkRequest, "Invalid bulk request for #{selected_ids.inspect}"
13+
end
14+
end
15+
16+
private
17+
18+
def bulk_request?
19+
action_name =~ /\Abulk_/
20+
end
21+
22+
def bulk_verification_token
23+
session[:_bulk_verification_token] ||= SecureRandom.base64(32)
24+
end
25+
26+
def bulk_verifier
27+
@_bulk_verifer ||= ActiveSupport::MessageVerifier.new(bulk_verification_token, serializer: JSON)
28+
end
29+
30+
def selected_ids
31+
@_selected_ids ||= params[:selected_ids].to_s.split(",").map(&:to_i).reject(&:zero?).take(50)
32+
end
33+
34+
def all_ids
35+
@_all_ids ||= bulk_verifier.verify(params[:all_ids])
36+
end
37+
38+
def verify_bulk_request
39+
selected_ids.all?(&method(:verify_bulk_request_id))
40+
end
41+
42+
def verify_bulk_request_id(id)
43+
all_ids.include?(id) || raise_bad_request(id)
44+
end
45+
46+
def raise_bad_request(id)
47+
raise BulkVerification::InvalidBulkRequest, "Invalid bulk request - #{id} not present in #{all_ids.inspect}"
48+
end
49+
end

app/views/admin/signatures/_signature.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<tr>
22
<td class="petition-id">
33
<label>
4-
<%= check_box_tag 'ids', signature.id, false, disabled: signature.creator? %> <%= signature.petition.id %>
4+
<%= check_box_tag 'id', signature.id, false, disabled: signature.creator? %> <%= signature.petition.id %>
55
</label>
66
</td>
77
<td class="petition-action"><%= link_to signature.petition.action, admin_petition_path(signature.petition) %></td>

app/views/admin/signatures/index.html.erb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,31 @@
3737
<%= will_paginate @signatures %>
3838

3939
<div class="inline-actions">
40+
<% signature_ids = bulk_verifier.generate(@signatures.map(&:id)) %>
41+
4042
<%= form_tag validate_admin_signatures_path(q: params[:q]), method: :post, class: 'action validate-action' do %>
41-
<%= hidden_field_tag 'ids', '' %>
43+
<%= hidden_field_tag 'selected_ids', '' %>
44+
<%= hidden_field_tag 'all_ids', signature_ids %>
4245
<%= submit_tag 'Validate', name: nil, class: 'button', disabled: true, data: { confirm: 'Validate selected signatures?' } %>
4346
<% end %>
4447

4548
<%= form_tag invalidate_admin_signatures_path(q: params[:q]), method: :post, class: 'action invalidate-action' do %>
46-
<%= hidden_field_tag 'ids', '' %>
49+
<%= hidden_field_tag 'selected_ids', '' %>
50+
<%= hidden_field_tag 'all_ids', signature_ids %>
4751
<%= submit_tag 'Invalidate', name: nil, class: 'button', disabled: true, data: { confirm: 'Invalidate selected signatures?' } %>
4852
<% end %>
4953

5054
<%= form_tag admin_signatures_path(q: params[:q]), method: :delete, class: 'action delete-action' do %>
51-
<%= hidden_field_tag 'ids', '' %>
55+
<%= hidden_field_tag 'selected_ids', '' %>
56+
<%= hidden_field_tag 'all_ids', signature_ids %>
5257
<%= submit_tag 'Delete', name: nil, class: 'button-warning', disabled: true, data: { confirm: 'Delete selected signatures?' } %>
5358
<% end %>
5459
</div>
5560

5661
<script>
5762
$(document).ready(function() {
5863
var $list = $('.signature-list');
59-
var $checkboxes = $list.find('input[name="ids"]');
64+
var $checkboxes = $list.find('input[name="id"]');
6065
var $selectAll = $list.find('input[name="select_all"]');
6166

6267
var selectedIds = function() {
@@ -116,7 +121,7 @@
116121
});
117122

118123
$('.inline-actions form').on('submit', function() {
119-
$(this).find('input[name="ids"]').val(selectedIds());
124+
$(this).find('input[name="selected_ids"]').val(selectedIds());
120125
});
121126
});
122127
</script>

config/application.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ class Application < Rails::Application
3838
# Remove the error wrapper from around the form element
3939
config.action_view.field_error_proc = -> (html_tag, instance) { html_tag }
4040

41-
# Add 503 Service Unavailable to the rescue response
41+
# Add additional exceptions to the rescue responses
4242
config.action_dispatch.rescue_responses.merge!(
43-
'Site::ServiceUnavailable' => :service_unavailable
43+
'Site::ServiceUnavailable' => :service_unavailable,
44+
'BulkVerification::InvalidBulkRequest' => :bad_request
4445
)
4546

4647
config.action_dispatch.default_headers.merge!('X-UA-Compatible' => 'IE=edge')

spec/controllers/admin/signatures_controller_spec.rb

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@
4141

4242
context "logged in as moderator user" do
4343
let(:user) { FactoryBot.create(:moderator_user) }
44-
before { login_as(user) }
44+
let(:token) { SecureRandom.base64(32) }
45+
let(:verifier) { ActiveSupport::MessageVerifier.new(token, serializer: JSON) }
46+
let(:signature_ids) { verifier.generate([signature.id]) }
47+
48+
before do
49+
login_as(user)
50+
session[:_bulk_verification_token] = token
51+
end
4552

4653
describe "GET /admin/signatures" do
4754
before { get :index, q: "Alice" }
@@ -168,7 +175,7 @@
168175
before do
169176
expect(Signature).to receive(:find).with([signature.id]).and_return([signature])
170177
expect(signature).to receive(:validate!).and_return(true)
171-
post :bulk_validate, ids: signature.id, q: "user@example.com"
178+
post :bulk_validate, selected_ids: signature.id, all_ids: signature_ids, q: "user@example.com"
172179
end
173180

174181
it "redirects to the search page" do
@@ -187,7 +194,7 @@
187194
expect(Signature).to receive(:find).with([signature.id]).and_return([signature])
188195
expect(signature).to receive(:validate!).and_raise(exception)
189196
expect(Appsignal).to receive(:send_exception).with(exception)
190-
post :bulk_validate, ids: signature.id, q: "user@example.com"
197+
post :bulk_validate, selected_ids: signature.id, all_ids: signature_ids, q: "user@example.com"
191198
end
192199

193200
it "redirects to the search page" do
@@ -198,14 +205,38 @@
198205
expect(flash[:alert]).to eq("Signatures could not be validated - please contact support")
199206
end
200207
end
208+
209+
context "when the signature ids hmac is missing" do
210+
before do
211+
expect(Signature).not_to receive(:find)
212+
end
213+
214+
it "returns a 400 Bad Request" do
215+
expect {
216+
delete :bulk_validate, selected_ids: signature.id, all_ids: "", q: "user@example.com"
217+
}.to raise_error(BulkVerification::InvalidBulkRequest, /Invalid bulk request for \[\d+\]/)
218+
end
219+
end
220+
221+
context "when the selected_ids param contains an invalid id" do
222+
before do
223+
expect(Signature).not_to receive(:find)
224+
end
225+
226+
it "returns a 400 Bad Request" do
227+
expect {
228+
delete :bulk_validate, selected_ids: "1,2", all_ids: signature_ids, q: "user@example.com"
229+
}.to raise_error(BulkVerification::InvalidBulkRequest, /Invalid bulk request - \d+ not present in \[\d+\]/)
230+
end
231+
end
201232
end
202233

203234
describe "POST /admin/signatures/invalidate" do
204235
context "when the signature is invalidated" do
205236
before do
206237
expect(Signature).to receive(:find).with([signature.id]).and_return([signature])
207238
expect(signature).to receive(:invalidate!).and_return(true)
208-
post :bulk_invalidate, ids: signature.id, q: "user@example.com"
239+
post :bulk_invalidate, selected_ids: signature.id, all_ids: signature_ids, q: "user@example.com"
209240
end
210241

211242
it "redirects to the search page" do
@@ -224,7 +255,7 @@
224255
expect(Signature).to receive(:find).with([signature.id]).and_return([signature])
225256
expect(signature).to receive(:invalidate!).and_raise(exception)
226257
expect(Appsignal).to receive(:send_exception).with(exception)
227-
post :bulk_invalidate, ids: signature.id, q: "user@example.com"
258+
post :bulk_invalidate, selected_ids: signature.id, all_ids: signature_ids, q: "user@example.com"
228259
end
229260

230261
it "redirects to the search page" do
@@ -235,14 +266,38 @@
235266
expect(flash[:alert]).to eq("Signatures could not be invalidated - please contact support")
236267
end
237268
end
269+
270+
context "when the signature ids hmac is missing" do
271+
before do
272+
expect(Signature).not_to receive(:find)
273+
end
274+
275+
it "returns a 400 Bad Request" do
276+
expect {
277+
delete :bulk_invalidate, selected_ids: signature.id, all_ids: "", q: "user@example.com"
278+
}.to raise_error(BulkVerification::InvalidBulkRequest, /Invalid bulk request for \[\d+\]/)
279+
end
280+
end
281+
282+
context "when the selected_ids param contains an invalid id" do
283+
before do
284+
expect(Signature).not_to receive(:find)
285+
end
286+
287+
it "returns a 400 Bad Request" do
288+
expect {
289+
delete :bulk_invalidate, selected_ids: "1,2", all_ids: signature_ids, q: "user@example.com"
290+
}.to raise_error(BulkVerification::InvalidBulkRequest, /Invalid bulk request - \d+ not present in \[\d+\]/)
291+
end
292+
end
238293
end
239294

240295
describe "DELETE /admin/signatures" do
241296
context "when the signature is destroyed" do
242297
before do
243298
expect(Signature).to receive(:find).with([signature.id]).and_return([signature])
244299
expect(signature).to receive(:destroy!).and_return(true)
245-
delete :bulk_destroy, ids: signature.id, q: "user@example.com"
300+
delete :bulk_destroy, selected_ids: signature.id, all_ids: signature_ids, q: "user@example.com"
246301
end
247302

248303
it "redirects to the search page" do
@@ -261,7 +316,7 @@
261316
expect(Signature).to receive(:find).with([signature.id]).and_return([signature])
262317
expect(signature).to receive(:destroy!).and_raise(exception)
263318
expect(Appsignal).to receive(:send_exception).with(exception)
264-
delete :bulk_destroy, ids: signature.id, q: "user@example.com"
319+
delete :bulk_destroy, selected_ids: signature.id, all_ids: signature_ids, q: "user@example.com"
265320
end
266321

267322
it "redirects to the search page" do
@@ -272,6 +327,30 @@
272327
expect(flash[:alert]).to eq("Signatures could not be removed - please contact support")
273328
end
274329
end
330+
331+
context "when the signature ids hmac is missing" do
332+
before do
333+
expect(Signature).not_to receive(:find)
334+
end
335+
336+
it "returns a 400 Bad Request" do
337+
expect {
338+
delete :bulk_destroy, selected_ids: signature.id, all_ids: "", q: "user@example.com"
339+
}.to raise_error(BulkVerification::InvalidBulkRequest, /Invalid bulk request for \[\d+\]/)
340+
end
341+
end
342+
343+
context "when the selected_ids param contains an invalid id" do
344+
before do
345+
expect(Signature).not_to receive(:find)
346+
end
347+
348+
it "returns a 400 Bad Request" do
349+
expect {
350+
delete :bulk_destroy, selected_ids: "1,2", all_ids: signature_ids, q: "user@example.com"
351+
}.to raise_error(BulkVerification::InvalidBulkRequest, /Invalid bulk request - \d+ not present in \[\d+\]/)
352+
end
353+
end
275354
end
276355
end
277356
end

0 commit comments

Comments
 (0)