Skip to content

Commit 7113a9e

Browse files
pglombardoPeter Giacomo Lombardo
andauthored
Fix: Prevent unauthorized deletion of anonymous pushes (#4703)
Anonymous pushes with deletable_by_viewer disabled were still deletable because nil owner matched nil current_user. Require an authenticated owner or explicit viewer-deletion permission. Co-authored-by: Peter Giacomo Lombardo <pglombardo@apnotic.com>
1 parent 8bbab0a commit 7113a9e

7 files changed

Lines changed: 100 additions & 2 deletions

File tree

app/controllers/api/v1/pushes_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ def audit
308308
https://docs.pwpush.com/docs/json-api/
309309
EOS
310310
def destroy
311-
if (@push.user == current_user) || @push.deletable_by_viewer
311+
if @push.deletable_by?(current_user)
312312
unless @push.expired?
313313
# Deletable by the owner or viewer
314314
@push.expire!

app/controllers/pushes_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def audit
332332

333333
def expire
334334
# Check if the push is deletable by the viewer or if the user is the owner
335-
unless @push.deletable_by_viewer || (@push.user == current_user)
335+
unless @push.deletable_by?(current_user)
336336
redirect_to :root, notice: I18n._("That push is not deletable by viewers and does not belong to you.")
337337
return
338338
end

app/models/push.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ def expire!
193193
save!
194194
end
195195

196+
# True when +user+ is the authenticated owner, or when viewer deletion is
197+
# explicitly enabled. Anonymous pushes have a nil owner; comparing two nils
198+
# must not count as ownership.
199+
def deletable_by?(user)
200+
(user.present? && user_id == user.id) || deletable_by_viewer == true
201+
end
202+
196203
def settings_for_kind
197204
if text?
198205
Settings.pw

test/integration/api/api_v2_pushes_test.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,27 @@ def test_destroy_allowed_for_anonymous_viewer_when_deletable_by_viewer
268268
assert push.reload.expired?
269269
end
270270

271+
def test_destroy_forbidden_for_anonymous_push_when_not_deletable_by_viewer
272+
post "/api/v2/pushes",
273+
params: {
274+
push: {
275+
payload: "anonymous-secret",
276+
deletable_by_viewer: false
277+
}
278+
},
279+
as: :json
280+
assert_response :created
281+
282+
token = JSON.parse(@response.body)["url_token"]
283+
push = Push.find_by!(url_token: token)
284+
assert_nil push.user_id
285+
assert_equal false, push.deletable_by_viewer
286+
287+
delete "/api/v2/pushes/#{token}", as: :json
288+
assert_response :unauthorized
289+
assert_not push.reload.expired?
290+
end
291+
271292
def test_create_requires_auth_when_allow_anonymous_disabled
272293
Settings.allow_anonymous = false
273294

test/integration/password/password_deletion_test.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,19 @@ def test_delete_already_expired_goes_to_expired_path
8080
assert_response :success
8181
assert response.body.include?("We apologize but this secret link has expired.")
8282
end
83+
84+
def test_anonymous_push_not_deletable_by_viewer_cannot_be_expired
85+
post pushes_path, params: {push: {kind: "text", payload: "testpw", deletable_by_viewer: "0"}}
86+
assert_response :redirect
87+
88+
push = Push.order(:created_at).last
89+
assert_nil push.user_id
90+
assert_equal false, push.deletable_by_viewer
91+
92+
delete expire_push_path(push)
93+
assert_response :redirect
94+
follow_redirect!
95+
assert_not push.reload.expired?
96+
assert_equal "testpw", push.payload
97+
end
8398
end

test/integration/password/password_json_deletion_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,36 @@ def test_deletion
7272
assert res.key?("views_remaining")
7373
assert_equal Settings.pw.expire_after_views_default - 1, res["views_remaining"]
7474
end
75+
76+
def test_anonymous_push_not_deletable_by_viewer_cannot_be_deleted_unauthenticated
77+
post passwords_path(format: :json), params: {
78+
password: {
79+
payload: "SUPER-SECRET",
80+
passphrase: "s3cr3t",
81+
deletable_by_viewer: "false"
82+
}
83+
}
84+
assert_response :success
85+
86+
res = JSON.parse(@response.body)
87+
token = res["url_token"]
88+
assert_equal false, res["deletable_by_viewer"]
89+
push = Push.find_by!(url_token: token)
90+
assert_nil push.user_id
91+
92+
# Passphrase still blocks reading
93+
get "/p/#{token}.json"
94+
assert_response :unauthorized
95+
96+
# Unauthenticated DELETE must not destroy the push
97+
delete "/p/#{token}.json"
98+
assert_response :unauthorized
99+
assert_not push.reload.expired?
100+
assert_equal "SUPER-SECRET", push.payload
101+
102+
# Intended recipient can still retrieve with passphrase
103+
get "/p/#{token}.json", params: {passphrase: "s3cr3t"}
104+
assert_response :success
105+
assert_equal "SUPER-SECRET", JSON.parse(@response.body)["payload"]
106+
end
75107
end

test/models/push_edit_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,27 @@ class PushEditTest < ActiveSupport::TestCase
392392
assert_not push.valid?
393393
assert_includes push.errors[:files], "You can only attach up to #{max_files} files per push."
394394
end
395+
396+
test "deletable_by? requires authenticated owner or explicit viewer deletion" do
397+
owner = users(:luca)
398+
other = users(:one)
399+
400+
owned = Push.create!(kind: "text", payload: "owned", user: owner, deletable_by_viewer: false)
401+
assert owned.deletable_by?(owner)
402+
assert_not owned.deletable_by?(other)
403+
assert_not owned.deletable_by?(nil)
404+
405+
anonymous = Push.create!(kind: "text", payload: "anon", deletable_by_viewer: false)
406+
assert_nil anonymous.user_id
407+
assert_not anonymous.deletable_by?(nil)
408+
assert_not anonymous.deletable_by?(other)
409+
410+
anonymous_deletable = Push.create!(kind: "text", payload: "anon-del", deletable_by_viewer: true)
411+
assert anonymous_deletable.deletable_by?(nil)
412+
assert anonymous_deletable.deletable_by?(other)
413+
414+
url_push = Push.create!(kind: "url", payload: "https://example.com", deletable_by_viewer: nil)
415+
assert_not url_push.deletable_by?(nil)
416+
assert_not url_push.deletable_by?(other)
417+
end
395418
end

0 commit comments

Comments
 (0)