0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/app/controllers/post_action_users_controller.rb
Alan Guo Xiang Tan aaa736f135 SECURITY: hide total_rows for restricted post action types
What is the problem?

- `PostActionUsersController#index` serves the list of users who
  performed a given post action, along with a
  `total_rows_post_action_users` count used for pagination.
- For restricted action types (flags, bookmarks, notify_user),
  `Guardian#can_see_post_actors?` correctly filters the user list to
  only the current user's own action.
- However, `total_rows_post_action_users` — read from the
  denormalized column on the post — was always included when the
  count exceeded the page size, regardless of permissions.
- A crafted request with a small `limit` param could cause
  `total_rows_post_action_users` to appear in the response, revealing
  how many total actions of that type exist on the post.

What is the solution?

- Only include `total_rows_post_action_users` in the response when
  the user can see post actors. The `idx_unique_actions` index
  enforces uniqueness on `(user_id, post_action_type_id, post_id,
  targets_topic)`, so a restricted user can have at most 2 actions
  per type per post — pagination is never needed for them.
- The public action types path (likes) is unchanged.
2026-03-19 15:46:24 +00:00

63 lines
1.8 KiB
Ruby
Vendored

# frozen_string_literal: true
class PostActionUsersController < ApplicationController
INDEX_LIMIT = 200
def index
params.require(:post_action_type_id)
params.require(:id)
post_action_type_id = params[:post_action_type_id].to_i
page = params[:page].to_i
page_size = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
# Find the post, and then determine if they can see the post (if deleted)
post = Post.with_deleted.find_by(id: params[:id].to_i)
guardian.ensure_can_see!(post)
post_actions =
post
.post_actions
.where(post_action_type_id: post_action_type_id)
.includes(:user)
.offset(page * page_size)
.order("post_actions.created_at ASC")
.limit(page_size)
post_actions =
DiscoursePluginRegistry.apply_modifier(:post_action_users_list, post_actions, post)
can_see_actors = guardian.can_see_post_actors?(post.topic, post_action_type_id)
if !can_see_actors
raise Discourse::InvalidAccess if current_user.blank?
post_actions = post_actions.where(user_id: current_user.id)
end
action_type = PostActionType.types.key(post_action_type_id)
total_count = post["#{action_type}_count"].to_i
post_actions = post_actions.to_a
data = {
post_action_users:
serialize_data(
post_actions,
PostActionUserSerializer,
unknown_user_ids: current_user_muting_or_ignoring_users(post_actions.map(&:user_id)),
),
}
data[:total_rows_post_action_users] = total_count if can_see_actors && total_count > page_size
render_json_dump(data)
end
private
def current_user_muting_or_ignoring_users(user_ids)
return [] if current_user.blank?
UserCommScreener.new(
acting_user: current_user,
target_user_ids: user_ids,
).actor_ignoring_or_muting_users
end
end